diff --git a/src/events/Ready.js b/src/events/Ready.js index 474bc760..a757011a 100644 --- a/src/events/Ready.js +++ b/src/events/Ready.js @@ -1,4 +1,7 @@ import logger from "../helpers/logger.js"; +import { resolve } from "node:path"; +import loadCronTasks from "../utils/loadCronTasks.js"; +import { CronManager } from "../services/cron/index.js"; export const data = { name: "ready", @@ -11,4 +14,11 @@ export const data = { */ export async function run(client) { logger.ready(client.user.tag + " is online!"); + + const taskPath = resolve(client.configService.get("paths.tasks")); + + const cronTasks = await loadCronTasks(taskPath); + + const cronManager = new CronManager(cronTasks); + await cronManager.init(); } diff --git a/src/helpers/birthdays.js b/src/helpers/birthdays.js deleted file mode 100644 index 749d3c04..00000000 --- a/src/helpers/birthdays.js +++ /dev/null @@ -1,71 +0,0 @@ -import { CronJob } from "cron"; -import useClient from "../utils/use-client.js"; -import UserModel from "../models/UserModel.js"; -import { createEmbed } from "../utils/create-embed.js"; -import logger from "./logger.js"; -import { getNoun } from "./functions.js"; - -const checkBirthdays = async () => { - const client = useClient(); - - const guilds = client.guilds.cache.values(); - const users = await client.adapter.find(UserModel, { birthdate: { $gt: 1 } }); - - const currentData = new Date(); - const currentYear = currentData.getFullYear(); - const currentMonth = currentData.getMonth(); - const currentDate = currentData.getDate(); - - for (const guild of guilds) { - try { - const data = await client.getGuildData(guild.id); - const channel = data.plugins.birthdays ? await client.channels.fetch(data.plugins.birthdays) : null; - - if (!channel) return; - - const userIDs = users.filter(u => guild.members.cache.has(u.id)).map(u => u.id); - - await Promise.all( - userIDs.map(async userID => { - const user = users.find(u => u.id === userID); - const userData = new Date(user.birthdate).getFullYear() <= 1970 ? new Date(user.birthdate * 1000) : new Date(user.birthdate); - const userYear = userData.getFullYear(); - const userMonth = userData.getMonth(); - const userDate = userData.getDate(); - - const age = currentYear - userYear; - - if (userDate === currentDate && userMonth === currentMonth) { - const embed = createEmbed({ - author: client.user.username, - fields: [ - { - name: client.translate("economy/birthdate:HAPPY_BIRTHDAY", { - lng: data.language, - }), - value: client.translate("economy/birthdate:HAPPY_BIRTHDAY_MESSAGE", { - lng: data.language, - user: user.id, - age: `**${age}** ${getNoun(age, [ - client.translate("misc:NOUNS:AGE:1", null, data.language), - client.translate("misc:NOUNS:AGE:2", null, data.language), - client.translate("misc:NOUNS:AGE:5", null, data.language), - ])}`, - }), - }, - ], - }); - - await channel.send({ embeds: [embed] }).then(m => m.react(" ")); - } - }), - ); - } catch (error) { - logger.error(error); - } - } -}; - -export async function init() { - new CronJob("0 5 * * *", checkBirthdays(), null, true, "Europe/Moscow"); -} diff --git a/src/helpers/checkReminds.js b/src/helpers/checkReminds.js deleted file mode 100644 index 71503aeb..00000000 --- a/src/helpers/checkReminds.js +++ /dev/null @@ -1,59 +0,0 @@ -import UserModel from "../models/UserModel"; -import useClient from "../utils/use-client"; - -const checkReminds = async () => { - const client = useClient(); - - client.adapter.find(UserModel, { reminds: { $gt: [] } }).then(users => { - for (const user of users) { - if (!client.users.cache.has(user.id)) client.users.fetch(user.id); - - client.cacheReminds.set(user.id, user); - } - }); - - client.cacheReminds.forEach(async user => { - const cachedUser = client.users.cache.get(user.id); - - if (!cachedUser) return; - - const reminds = user.reminds, - mustSent = reminds.filter(r => r.sendAt < Math.floor(Date.now() / 1000)); - - if (!mustSent.length) return; - - mustSent.forEach(r => { - const embed = client.embed({ - author: client.translate("general/remindme:EMBED_TITLE"), - fields: [ - { - name: client.translate("general/remindme:EMBED_CREATED"), - value: ``, - inline: true, - }, - { - name: client.translate("general/remindme:EMBED_TIME"), - value: ``, - inline: true, - }, - { - name: client.translate("common:MESSAGE"), - value: r.message, - }, - ], - }); - - cachedUser.send({ embeds: [embed] }).then(() => { - client.adapter.updateOne(UserModel, { id: user.id }, { $pull: { reminds: { _id: r._id } } }); - }); - }); - - if (!user.reminds.length) client.cacheReminds.delete(user.id); - }); -}; - -export const init = async () => { - setInterval(async () => { - await checkReminds(); - }, 1000); -}; \ No newline at end of file diff --git a/src/helpers/tasks/birthdays.js b/src/helpers/tasks/birthdays.js new file mode 100644 index 00000000..99d558d9 --- /dev/null +++ b/src/helpers/tasks/birthdays.js @@ -0,0 +1,70 @@ +import useClient from "../../utils/use-client.js"; +import UserModel from "../../models/UserModel.js"; +import { createEmbed } from "../../utils/create-embed.js"; +import logger from "../logger.js"; +import { getNoun } from "../functions.js"; + +export const data = { + name: "birthdays", + task: async () => { + const client = useClient(); + + const guilds = client.guilds.cache.values(); + const users = await client.adapter.find(UserModel, { birthdate: { $gt: 1 } }); + + const currentData = new Date(); + const currentYear = currentData.getFullYear(); + const currentMonth = currentData.getMonth(); + const currentDate = currentData.getDate(); + + for (const guild of guilds) { + try { + const data = await client.getGuildData(guild.id); + const channel = data.plugins.birthdays ? await client.channels.fetch(data.plugins.birthdays) : null; + + if (!channel) return; + + const userIDs = users.filter(u => guild.members.cache.has(u.id)).map(u => u.id); + + await Promise.all( + userIDs.map(async userID => { + const user = users.find(u => u.id === userID); + const userData = new Date(user.birthdate).getFullYear() <= 1970 ? new Date(user.birthdate * 1000) : new Date(user.birthdate); + const userYear = userData.getFullYear(); + const userMonth = userData.getMonth(); + const userDate = userData.getDate(); + + const age = currentYear - userYear; + + if (userDate === currentDate && userMonth === currentMonth) { + const embed = createEmbed({ + author: client.user.username, + fields: [ + { + name: client.translate("economy/birthdate:HAPPY_BIRTHDAY", { + lng: data.language, + }), + value: client.translate("economy/birthdate:HAPPY_BIRTHDAY_MESSAGE", { + lng: data.language, + user: user.id, + age: `**${age}** ${getNoun(age, [ + client.translate("misc:NOUNS:AGE:1", null, data.language), + client.translate("misc:NOUNS:AGE:2", null, data.language), + client.translate("misc:NOUNS:AGE:5", null, data.language), + ])}`, + }), + }, + ], + }); + + await channel.send({ embeds: [embed] }).then(m => m.react(" ")); + } + }), + ); + } catch (error) { + logger.error(error); + } + } + }, + schedule: "0 5 * * *", +}; diff --git a/src/helpers/tasks/checkReminds.js b/src/helpers/tasks/checkReminds.js new file mode 100644 index 00000000..1fa09a71 --- /dev/null +++ b/src/helpers/tasks/checkReminds.js @@ -0,0 +1,68 @@ +import UserModel from "../../models/UserModel.js"; +import useClient from "../../utils/use-client.js"; +import { createEmbed } from "../../utils/index.js"; + +export const data = { + name: "checkReminds", + task: async () => { + const client = useClient(); + + client.adapter.find(UserModel, { reminds: { $gt: [] } }).then(users => { + for (const user of users) { + if (!client.users.cache.has(user.id)) client.users.fetch(user.id); + + client.cacheReminds.set(user.id, user); + } + }); + + client.cacheReminds.forEach(async user => { + const cachedUser = client.users.cache.get(user.id); + + if (!cachedUser) return; + + const reminds = user.reminds, + mustSent = reminds.filter(r => r.sendAt < Math.floor(Date.now() / 1000)); + + if (!mustSent.length) return; + + mustSent.forEach(r => { + const embed = createEmbed({ + author: { + name: client.translate("general/remindme:EMBED_TITLE"), + }, + fields: [ + { + name: client.translate("general/remindme:EMBED_CREATED"), + value: ``, + inline: true, + }, + { + name: client.translate("general/remindme:EMBED_TIME"), + value: ``, + inline: true, + }, + { + name: client.translate("common:MESSAGE"), + value: r.message, + }, + ], + }); + + cachedUser + .send({ + embeds: [embed], + }) + .then(() => { + client.adapter.updateOne(UserModel, { id: user.id }, { $pull: { reminds: { _id: r._id } } }); + }); + }); + + user.reminds = user.reminds.filter(r => r.sendAt >= Math.floor(Date.now() / 1000)); + + await user.save(); + + if (!user.reminds.length) client.cacheReminds.delete(user.id); + }); + }, + schedule: "* * * * * *", +}; diff --git a/src/services/cron/index.js b/src/services/cron/index.js new file mode 100644 index 00000000..6bf721d3 --- /dev/null +++ b/src/services/cron/index.js @@ -0,0 +1,61 @@ +import { CronJob } from "cron"; +import logger from "../../helpers/logger.js"; + +export class CronManager { + constructor(tasks) { + this.tasks = tasks; + this.jobs = new Map(); + } + + async init() { + if (!this.tasks.length) { + logger.warn("No cron tasks to schedule."); + return; + } + + for (const task of this.tasks) { + const taskJob = this.jobs.get(task.name); + if (taskJob?.isRunning) { + logger.warn(`Cron task "${task.name}" is already running.`); + continue; + } + + const job = new CronJob( + task.schedule, + async () => { + if (this.jobs.get(task.name)?.isError) { + return; + } + + try { + await task.task(); + } catch (error) { + logger.error(`Error executing cron task "${task.name}":`, error); + this.jobs.get(task.name).isError = true; + } + }, + null, + false, + "Europe/Moscow", + ); + + job.start(); + this.jobs.set(task.name, { + job, + isRunning: true, + isError: false, + }); + } + logger.log(`Cron tasks scheduled: ${this.jobs.size}`); + } + + stopAll() { + if (!this.jobs.length) return; + + // eslint-disable-next-line no-unused-vars + for (const [_, jobInfo] of this.jobs.entries()) { + jobInfo.job.stop(); + } + logger.log("All cron jobs stopped."); + } +} diff --git a/src/utils/loadCronTasks.js b/src/utils/loadCronTasks.js new file mode 100644 index 00000000..ff50c4e7 --- /dev/null +++ b/src/utils/loadCronTasks.js @@ -0,0 +1,41 @@ +import logger from "../helpers/logger.js"; +import { getFilePaths } from "./get-path.js"; +import { toFileURL } from "./resolve-file.js"; + +const loadCronTasks = async taskPath => { + try { + const filePaths = (await getFilePaths(taskPath, true)).filter(file => file.endsWith(".js")); + + const tasks = []; + + for (const filePath of filePaths) { + const { data } = await import(toFileURL(filePath)); + + if (!data) continue; + + if (!data.name) { + logger.warn("No name found in task:", filePath); + continue; + } + + if (!data.schedule) { + logger.warn("No schedule found in task:", filePath); + continue; + } + + if (typeof data.task !== "function") { + logger.warn("Task is not a function:", filePath); + continue; + } + + tasks.push(data); + } + + return tasks; + } catch (error) { + logger.error("Error loading cron tasks:", error); + return []; + } +}; + +export default loadCronTasks;