diff --git a/.gitignore b/.gitignore index 8934ffc..03db4eb 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,8 @@ yarn-debug.log* yarn-error.log* lerna-debug.log* .pnpm-debug.log* -Logging +logging +logs.json # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json diff --git a/src/api/controllers/info.controller.ts b/src/api/controllers/info.controller.ts index 0d02c75..ee4363f 100644 --- a/src/api/controllers/info.controller.ts +++ b/src/api/controllers/info.controller.ts @@ -75,6 +75,7 @@ export const infoController = new Elysia({ ); function readLogFile() { - const logPath = path.join(srcFolder, "Logging", "logs.json"); - return JSON.parse(fs.readFileSync(logPath, "utf-8")); + const logPath = path.join(srcFolder, "logging", "logs.json"); + const lines = fs.readFileSync(logPath, "utf-8").split("\n").filter(Boolean); + return lines.map((line) => JSON.parse(line)); } diff --git a/src/api/controllers/trigger.controller.ts b/src/api/controllers/trigger.controller.ts index e2e3339..905290c 100644 --- a/src/api/controllers/trigger.controller.ts +++ b/src/api/controllers/trigger.controller.ts @@ -27,7 +27,7 @@ export const triggerController = new Elysia({ .post( "/wipe-logs", async () => { - const logPath = path.join(import.meta.dir, "..", "..", "Logging", "logs.json"); + const logPath = path.join(import.meta.dir, "..", "..", "logging", "logs.json"); fs.writeFileSync(logPath, JSON.stringify([]), "utf8"); return { message: "Wiped all logs!" }; }, diff --git a/src/app.ts b/src/app.ts index 034af4b..ec5fcc3 100644 --- a/src/app.ts +++ b/src/app.ts @@ -4,8 +4,6 @@ import { Client } from "#structures/index"; import { GatewayIntentBits } from "discord.js"; import { registerEvents, RSA, updateBotStats } from "#utils/index"; import { runChecks } from "#checks/run"; -import path from "path"; -import fs from "fs"; import { syncAnilistUsers, type WorkerResponseUnion } from "#workers/index"; import { env } from "#env"; import { eq, sql } from "drizzle-orm"; @@ -29,20 +27,6 @@ async function start(token: string | undefined) { client.login(token); await updateBotStats(client); - const logPath = path.join(import.meta.dir, 'Logging/logs.json') - const currentDate = new Date().toISOString().replace(/T/, ' ').replace(/\..+/, '') - - if (!fs.existsSync(path.join(import.meta.dir, 'Logging'))) - fs.mkdirSync(path.join(import.meta.dir, 'Logging')) - - if (!fs.existsSync(logPath)) - fs.writeFileSync(logPath, JSON.stringify( - [{ - date: currentDate, - user: "SYSTEM_LOGGER", - info: "Initialized log!" - }])); - env().UPTIME = Date.now(); } @@ -72,7 +56,7 @@ workerManager.onmessage = async (e) => { switch (data.type) { case 'LOG': { - client.log(data.text, data.category); + client.logger.log(data.level, data.text); break; } case "SYNC": { @@ -84,4 +68,4 @@ workerManager.onmessage = async (e) => { break; } } -}; \ No newline at end of file +}; diff --git a/src/caching/redis.ts b/src/caching/redis.ts index 1376170..9b65eb3 100644 --- a/src/caching/redis.ts +++ b/src/caching/redis.ts @@ -1,5 +1,5 @@ import { createClient } from "redis"; -import { client } from "app"; +import { client } from "#src/app"; import { env } from "#env"; const host = env().NODE_ENV === "docker" ? "dragonfly" : "localhost"; @@ -16,7 +16,7 @@ redis.on("error", (err) => { }); redis.on("connect", () => { - client.log(`Connected to ${host}!`, "info"); + client.logger.info(`Connected to ${host}!`); redis.set("test", "test"); }); diff --git a/src/checks/run.ts b/src/checks/run.ts index 5f892de..e03c076 100644 --- a/src/checks/run.ts +++ b/src/checks/run.ts @@ -15,22 +15,24 @@ export async function runChecks(client: Client) { }), ) ).flat() - client.log(`Running ${checks.length} checks...`, "info") + client.logger.info("Running checks", { type: "check", total: checks.length }); for (const check of checks) { try { check.run() - client.log(`[✅] Check "${check.name}" passed.`, "info") + client.logger.info("Check passed", { type: "check", name: check.name, optional: check.optional }); } catch (e) { if (check.optional === true) { - client.log(`[⚠️] Optional check "${check.name}" failed. This may or may not cause problems in the future. - > Purpose: ${check.description} - > Why: ${e} - `, "warn") + client.logger.log("warn", "Optional check failed", { + type: "check", + name: check.name, + purpose: check.description, + why: serializeError(e), + }); } else { - throw new Error(`[❌] FATAL: Critical check "${check.name}" failed. Cannot continue. + throw new Error(`Critical check "${check.name}" failed > Purpose: ${check.description} > Why: ${e} `) @@ -38,5 +40,16 @@ export async function runChecks(client: Client) { } } - client.log(`Checks passed!`, "info") + client.logger.info("Checks passed!"); } + +function serializeError(e: unknown) { + if (e instanceof Error) { + return { + message: e.message, + stack: e.stack, + name: e.name, + }; + } + return e; +} \ No newline at end of file diff --git a/src/checks/verifyEnv.ts b/src/checks/verifyEnv.ts index a5c5877..31c4dcb 100644 --- a/src/checks/verifyEnv.ts +++ b/src/checks/verifyEnv.ts @@ -22,8 +22,7 @@ const clientIDCheck = new Check({ const guildIDCheck = new Check({ name: 'Guild ID Check', - description: `Check if process.env.GUILD_ID is present and valid. - This is required for slash commands to be instantly visible in a guild when developing.`, + description: `Check if process.env.GUILD_ID is present and valid. This is required for slash commands to be instantly visible in a guild when developing.`, optional: true, run: () => { if (!process.env.GUILD_ID) diff --git a/src/commands/activity.ts b/src/commands/activity.ts index 7591b76..7cc6fea 100644 --- a/src/commands/activity.ts +++ b/src/commands/activity.ts @@ -38,7 +38,7 @@ export default { }, interaction.ALtoken) ).data.User; - if (!uData?.id) throw new YuukoError("Couldn't find user id.", vars); + if (!uData?.id) throw new YuukoError("Couldn't find user id.", { vars }); vars.userid = uData?.id; } @@ -48,7 +48,7 @@ export default { } = await graphQLRequest("Activity", vars, interaction.ALtoken); if (!data) { - throw new YuukoError("Couldn't find any data.", vars); + throw new YuukoError("Couldn't find any data.", { vars }); } const embed = new EmbedBuilder().setTimestamp(data?.createdAt * 1000); @@ -161,4 +161,4 @@ function replaceUrls(input: string): string { } return newString; -} \ No newline at end of file +} diff --git a/src/commands/airing.ts b/src/commands/airing.ts index fabe1b6..3d97095 100644 --- a/src/commands/airing.ts +++ b/src/commands/airing.ts @@ -37,7 +37,7 @@ export default { if (period) { // @ts-ignore can't think of any other way to get around this airingIn = ms(period); - if (!airingIn) throw new YuukoError("Invalid time format. See `/help` for more information.", { period }); + if (!airingIn) throw new YuukoError("Invalid time format. See `/help` for more information.", { vars: { period } }); } // ^ Get current day and time in UTC @@ -75,7 +75,7 @@ export default { data: { Page: data }, headers, } = await graphQLRequest("Airing", vars); - if (!data || !data.airingSchedules) throw new YuukoError("No airing anime found.", vars); + if (!data || !data.airingSchedules) throw new YuukoError("No airing anime found.", { vars }); const { airingSchedules } = data; const chunkSize = 5; diff --git a/src/commands/anime.ts b/src/commands/anime.ts index 53a1b40..daf850f 100644 --- a/src/commands/anime.ts +++ b/src/commands/anime.ts @@ -34,16 +34,13 @@ export default { if (cachedId) { animeIdFound = true; vars.aID = parseInt(cachedId); - client.log(`Found cached data for ${normalizedQuery}, ID ${vars.aID}`, "debug"); + client.logger.debug("Series cache hit", { type: "commandDebug", command: name, query: normalizedQuery, seriesId: vars.aID, mediaType: "ANIME" }) } } else { vars.aID = hookData.id; } - client.log(`Anime ID: ${vars.aID}`, "debug"); - - client.log(`Querying Redis with hook animeId ${vars.aID}`, "debug"); const cacheData = (await redis.json.get(`_anime-${vars.aID}`)) as AnimeQuery["Media"] | null; if (cacheData) { @@ -55,19 +52,18 @@ export default { if (mediaListEntry) cacheData.mediaListEntry = mediaListEntry; } - client.log("Found cache data, returning data...", "debug"); + client.logger.debug("User cache hit", { type: "commandDebug", command: name, seriesId: vars.aID, anilistId: interaction.alID }) - return void handleData({ media: cacheData }, interaction, "ANIME"); + return void handleData({ media: cacheData }, interaction, client, "ANIME"); } - client.log("No cache found, fetching from CringeQL", "debug"); const { data: { Media: data }, headers, } = await graphQLRequest("Anime", vars, interaction.ALtoken); if (!data) { - throw new YuukoError("No anime found.", vars); + throw new YuukoError("No anime found.", { vars }); } if (!animeIdFound) redis.set(`_animeId-${vars.query}`, data.id); @@ -79,9 +75,9 @@ export default { redis.set(`_animeId-${normalize(synonym)}`, data.id); } if (redisData.nextAiringEpisode?.airingAt) { - client.log(`Expiring anime-${redisData.id} at ${redisData.nextAiringEpisode.airingAt}`, "debug"); + client.logger.debug("Adding expiration date", { type: "commandDebug", command: name, seriesId: redisData.id, airingAt: redisData.nextAiringEpisode.airingAt }) redis.expireAt(`_anime-${data.id}`, redisData.nextAiringEpisode.airingAt); } - return void await handleData({ media: data, headers: headers }, interaction, "ANIME", hookData); + return void await handleData({ media: data, headers: headers }, interaction, client, "ANIME", hookData); }, } satisfies Command<{ id?: number, anime?: string }>; diff --git a/src/commands/auth.ts b/src/commands/auth.ts index d7dfff0..424fcf1 100644 --- a/src/commands/auth.ts +++ b/src/commands/auth.ts @@ -42,7 +42,7 @@ export default { const user = (await db.select().from(anilistUser).where(eq(anilistUser.discordId, interaction.user.id)).limit(1))[0]; if (subcommandType === "wipe") { - if (!user) throw new YuukoError("You don't have an AniList account bound to your Discord account.", null, true) + if (!user) throw new YuukoError("You don't have an AniList account bound to your Discord account.", { ephemeral: true }) await db.delete(anilistUser).where(eq(anilistUser.discordId, interaction.user.id)); await updateBotStats(client); diff --git a/src/commands/character.ts b/src/commands/character.ts index fc75ecb..f0dfe4c 100644 --- a/src/commands/character.ts +++ b/src/commands/character.ts @@ -24,7 +24,7 @@ export default { headers, } = await graphQLRequest("Character", { charName }); - if (!data) throw new YuukoError("Couldn't find this character.", { charName }); + if (!data) throw new YuukoError("Couldn't find this character.", { vars: { charName } }); const embeds = []; const description = diff --git a/src/commands/makeactivity.ts b/src/commands/makeactivity.ts index 472c4b4..bf5fcb3 100644 --- a/src/commands/makeactivity.ts +++ b/src/commands/makeactivity.ts @@ -93,7 +93,7 @@ export default { const statusData = hookData?.subcommandType === "status" ? hookData : undefined; const statusText = statusData?.text ?? interaction.options.getString("text", true); const vars = { text: getEmojis(statusText), asHtml: true }; - if (!interaction.ALtoken) throw new YuukoError("No Anilist token found.", null, true); + if (!interaction.ALtoken) throw new YuukoError("No Anilist token found.", { ephemeral: true }); const { data: { SaveTextActivity: data }, @@ -126,13 +126,13 @@ export default { } } - if (!interaction.ALtoken) throw new YuukoError("No Anilist token found.", null, true); + if (!interaction.ALtoken) throw new YuukoError("No Anilist token found.", { ephemeral: true }); const { data: { SaveMediaListEntry: data }, headers, } = await graphQLRequest("SaveMediaList", vars, interaction.ALtoken); - if (!data) throw new YuukoError("Something went wrong while making the activity.", null, true); + if (!data) throw new YuukoError("Something went wrong while making the activity.", { ephemeral: true }); const mediaListActivity = new EmbedBuilder() .setURL(`https://anilist.co/${data?.media?.type || ""}/${data?.mediaId || ""}`) .setTitle(`${data.user?.name || "Unknown"} added ${data?.media?.title?.userPreferred || "Unknown"} to ${data?.status || "Unknown"}!`) diff --git a/src/commands/manga.ts b/src/commands/manga.ts index 4c24718..164834b 100644 --- a/src/commands/manga.ts +++ b/src/commands/manga.ts @@ -38,7 +38,7 @@ export default { if (cachedId) { mangaIdFound = true; vars.mID = parseInt(cachedId); - client.log(`Found cached data for ${normalizedQuery}, ID ${vars.mID}`, "debug"); + client.logger.debug("Series cache hit", { query: normalizedQuery, seriesId: vars.mID, type: "generic", mediaType: "MANGA" }) } } else { @@ -50,15 +50,15 @@ export default { if (cacheData) { if (interaction.alID) { const _mediaListEntry = await redis.json.get(`_user${interaction.alID}-MANGA`) as Record; - if (!vars.mID) throw new YuukoError("No mID found in cache data.", vars); + if (!vars.mID) throw new YuukoError("No mID found in cache data.", { vars }); const mediaListEntry = _mediaListEntry ? _mediaListEntry[vars.mID] : null; if (mediaListEntry) cacheData.mediaListEntry = mediaListEntry; } - client.log("[MangaCmd] Found cache data, returning data...", "debug"); - return void handleData({ media: cacheData }, interaction, "MANGA"); - } - client.log("[MangaCmd] No cache found, fetching from CringeQL", "debug"); + client.logger.debug("User cache hit", { seriesId: vars.mID, anilistId: interaction.alID, type: "generic" }) + + return void handleData({ media: cacheData }, interaction, client, "MANGA"); + } const { data: { Media: data }, @@ -66,17 +66,20 @@ export default { } = await graphQLRequest("Manga", vars, interaction.ALtoken); if (!data) { - throw new YuukoError("Couldn't find any data.", vars); + throw new YuukoError("Couldn't find any data.", { vars }); } if (!mangaIdFound) redis.set(`_mangaId-${vars.query}`, data.id); + const { mediaListEntry, ...redisData } = data; redis.json.set(`_manga-${data.id}`, "$", redisData); redis.expireAt(`_manga-${redisData.id}`, new Date(Date.now() + 604800000)) + for (const synonym of redisData.synonyms || []) { if (!synonym) continue; redis.set(`_mangaId-${normalize(synonym)}`, data.id.toString()); } - return void handleData({ media: data, headers: headers }, interaction, "MANGA", hookData); + + return void handleData({ media: data, headers: headers }, interaction, client, "MANGA", hookData); }, -} satisfies Command<{ id?: number, manga?: string }>; \ No newline at end of file +} satisfies Command<{ id?: number, manga?: string }>; diff --git a/src/commands/pixel_jumble.ts b/src/commands/pixel_jumble.ts index a4ade9b..7bb5918 100644 --- a/src/commands/pixel_jumble.ts +++ b/src/commands/pixel_jumble.ts @@ -50,7 +50,7 @@ export default { const vars = { type, userId: interaction.alID, chunk: Math.floor(Math.random() * totalSize) }; const { data: { MediaListCollection: data } } = await graphQLRequest("PixelJumble", vars); - if (!data || !data.lists || data.lists.length < 1) throw new YuukoError("Couldn't find any data from the user specified.", vars); + if (!data || !data.lists || data.lists.length < 1) throw new YuukoError("Couldn't find any data from the user specified.", { vars }); const allMediaItems = data.lists .flatMap(list => list?.entries); diff --git a/src/commands/recent.ts b/src/commands/recent.ts index f683c7b..fd65dfe 100644 --- a/src/commands/recent.ts +++ b/src/commands/recent.ts @@ -32,7 +32,7 @@ export default { if (!userName) { // We try to use the one the user set - if (!interaction.alID) throw new YuukoError("You have yet to set an AniList token.", null, true) + if (!interaction.alID) throw new YuukoError("You have yet to set an AniList token.", { ephemeral: true }) vars.userId = interaction.alID; } else { vars.user = userName; @@ -41,7 +41,7 @@ export default { const { data: { Page: data }, } = await graphQLRequest("RecentChart", vars, interaction.ALtoken); - if (!data?.mediaList) throw new YuukoError("Unable to find specified user.", vars, true); + if (!data?.mediaList) throw new YuukoError("Unable to find specified user.", { vars, ephemeral: true }); await interaction.deferReply(); const parsedData = []; @@ -56,6 +56,8 @@ export default { parsedData.push({ status: `${status}\n${title}`, imageUrl: cover }); } + client.logger.debug("Recent command", { type: "generic", total: parsedData.length, userName: vars.user }); + const lib = client.modules.getModule("modules"); const enc = new TextEncoder(); diff --git a/src/commands/recommend.ts b/src/commands/recommend.ts index 705368a..c37c5f1 100644 --- a/src/commands/recommend.ts +++ b/src/commands/recommend.ts @@ -39,7 +39,7 @@ export default { data: { MediaListCollection: data }, } = await graphQLRequest("GetMediaCollection", vars); - if (!data || !data.lists || data.lists.length < 1) throw new YuukoError("Couldn't find any data from the user specified.", vars); + if (!data || !data.lists || data.lists.length < 1) throw new YuukoError("Couldn't find any data from the user specified.", { vars }); // ^ We filter out the Planning list for (const MediaList of data.lists.filter((MediaList) => MediaList!.name != "Planning")) { @@ -55,13 +55,13 @@ export default { } = await graphQLRequest("Recommendations", recommendationVars); if (!recommendationData || !recommendationData.media) { - throw new YuukoError("Couldn't find any data.", recommendationVars); + throw new YuukoError("Couldn't find any data.", { vars: recommendationVars }); } // ^ Filter out the Planning list const recommendations = recommendationData.media.filter((Media) => Media!.title); const random = Math.floor(Math.random() * Math.floor(recommendations.length)); const recommendedSeries = recommendations[random]; - if (!recommendedSeries) throw new YuukoError("Couldn't find any data.", recommendationVars); + if (!recommendedSeries) throw new YuukoError("Couldn't find any data.", { vars: recommendationVars }); switch (type) { case "ANIME": diff --git a/src/commands/staff.ts b/src/commands/staff.ts index cf676e3..ce964cd 100644 --- a/src/commands/staff.ts +++ b/src/commands/staff.ts @@ -23,7 +23,7 @@ export default { data: { Staff: data }, headers } = await graphQLRequest("Staff", { staffName }); - if (!data) throw new YuukoError("Couldn't find any data.", { staffName }); + if (!data) throw new YuukoError("Couldn't find any data.", { vars: { staffName } }); const staffMedia = data.staffMedia; const characterMedia = data.characterMedia; diff --git a/src/commands/studio.ts b/src/commands/studio.ts index 116bd8b..9c674f1 100644 --- a/src/commands/studio.ts +++ b/src/commands/studio.ts @@ -24,7 +24,7 @@ export default { headers, } = await graphQLRequest("Studio", { query: studioName }); - if (!data || !data.media?.nodes) throw new YuukoError("Couldn't find any data.", { studioName }); + if (!data || !data.media?.nodes) throw new YuukoError("Couldn't find any data.", { vars: { studioName } }); let animes: string[] | string = []; for (const anime of data.media.nodes) animes = animes.concat(`[${SeriesTitle(anime?.title || undefined)}]` + `(https://anilist.co/anime/${anime!.id})`); diff --git a/src/commands/synclists.ts b/src/commands/synclists.ts index e51bf1a..9a9be50 100644 --- a/src/commands/synclists.ts +++ b/src/commands/synclists.ts @@ -7,6 +7,7 @@ import { db } from "#database/db"; import { normalize, graphQLRequest, type AlwaysExist, type CacheEntry, type GraphQLResponse, YuukoError, getSubcommandOption } from "#utils/index"; import { eq } from "drizzle-orm"; import { mediaStats, mediaStatUsers } from "#database/models"; +import { client } from "#src/app"; const name = "synclists"; const usage = "/synclists"; @@ -131,6 +132,7 @@ export async function handleSyncing( } const mediasArray = Array.from(bulkMedia); + client.logger.debug("Sync media stats", { type: "generic", total: mediasArray.length, anilistId: alID, mediaType: type }); if (mediasArray.length === 0) return; // bulk insert media_id, do nothing if exists already await db @@ -138,7 +140,9 @@ export async function handleSyncing( .values(mediasArray) .onConflictDoNothing(); + const userFromMedias = mediasArray.map((m) => ({ mediaId: m.mediaId, anilistId: alID })); + client.logger.debug("Sync media stats users", { type: "generic", total: userFromMedias.length, anilistId: alID, mediaType: type }); // bulk insert user into given media(s) await db .insert(mediaStatUsers) diff --git a/src/commands/user.ts b/src/commands/user.ts index 79507a2..450e0a4 100644 --- a/src/commands/user.ts +++ b/src/commands/user.ts @@ -33,7 +33,7 @@ export default { if (interaction.alID) { vars = { userid: interaction.alID }; } else { - throw new YuukoError("You have yet to set an AniList token.", null, true); + throw new YuukoError("You have yet to set an AniList token.", { ephemeral: true }); } } @@ -41,7 +41,7 @@ export default { const { data, headers } = await graphQLRequest("User", vars, interaction.ALtoken); const response = data.User; - if (!response) throw new YuukoError("Couldn't find any data.", vars); + if (!response) throw new YuukoError("Couldn't find any data.", { vars }); const titleEmbed = new EmbedBuilder() // TODO: Fix depricated function calls 101 diff --git a/src/events/$ready.ts b/src/events/$ready.ts index 9a4fffa..f1dcc53 100644 --- a/src/events/$ready.ts +++ b/src/events/$ready.ts @@ -15,8 +15,8 @@ const ready = new YuukoEvent({ await registerCommands(client); await registerComponents(client); - client.log(`${client.user.tag} is ready!`, "Info"); + client.logger.info("Bot ready", { type: "startup", user: client.user.tag }); } }); -export default [ready]; \ No newline at end of file +export default [ready]; diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts index 13c58ec..5e81447 100644 --- a/src/events/interactionCreate.ts +++ b/src/events/interactionCreate.ts @@ -1,7 +1,7 @@ import { type Interaction, Collection, MessageFlags, time } from "discord.js"; -import { embedError, logging, YuukoError } from "#utils/index"; -import { type Client, type ClientCommand, type UsableInteraction, YuukoEvent, type Middleware } from "#structures/index"; +import { embedError, YuukoError } from "#utils/index"; +import { type Client, type Command, type UsableInteraction, YuukoEvent, type Middleware } from "#structures/index"; /* discord doesn't have the commands property in their class for somea reason */ const interactionCreate = new YuukoEvent({ @@ -14,16 +14,22 @@ const interactionCreate = new YuukoEvent({ // We run the command based on the interaction const command = client.commands.find((cmd) => cmd.name == interaction.commandName); if (!command) return; - client.log(`Interaction received in: ${Date.now() - interaction.createdTimestamp}ms`, "debug"); + checkCooldown(client, command, interaction); - const start = performance.now(); + const args = await runMiddlewares(command.middlewares, interaction, client); - client.log(`Ran middleware in: ${Math.round(performance.now() - start)}ms`, "Debug"); + + if (command.middlewares) { + client.logger.debug("Middleware execution", { + type: "generic", + command: command.name, + middlewares: command.middlewares?.map((m) => m.name), + }); + } if (args.isCommand() && args.isChatInputCommand()) { - logging(command, interaction); + client.logger.logCommand(command, interaction); await command.run({ interaction: args, client }); - client.log(`Ran command ${command.name} in: ${Date.now() - interaction.createdTimestamp}ms`, "Debug"); } // Check for autocomplete @@ -42,9 +48,10 @@ const interactionCreate = new YuukoEvent({ } } catch (e: any) { - console.error(e); + client.logger.error(e); if (e instanceof YuukoError) { + client.logger.error("Yuuko Error", { type: "generic", message: e.message, vars: e.vars, cause: e.cause }); if (!interaction.isCommand()) return; if (interaction.deferred || interaction.replied) @@ -67,7 +74,7 @@ const interactionCreate = new YuukoEvent({ } - function checkCooldown(client: Client, command: ClientCommand, interaction: UsableInteraction): UsableInteraction { + function checkCooldown(client: Client, command: Command, interaction: UsableInteraction): UsableInteraction { if (!interaction.isChatInputCommand()) return interaction; const commandCooldown = client.cooldowns.get(command.name); if (!commandCooldown) { @@ -79,7 +86,7 @@ const interactionCreate = new YuukoEvent({ console.log(cooldownExpires); if (!cooldownExpires) return interaction; if (cooldownExpires > Date.now()) { - throw new YuukoError(`User ${interaction.user.tag} is on cooldown for command ${command.name}`, null, false, `Cooldown expires on ${time(Math.ceil(cooldownExpires / 1000), "f")} (${time(Math.ceil(cooldownExpires / 1000), "R")})`); + throw new YuukoError(`User ${interaction.user.tag} is on cooldown for command ${command.name}`, { cause: `Cooldown expires on ${time(Math.ceil(cooldownExpires / 1000), "f")} (${time(Math.ceil(cooldownExpires / 1000), "R")})` }); } } return interaction; @@ -87,4 +94,4 @@ const interactionCreate = new YuukoEvent({ } }); -export default [interactionCreate]; \ No newline at end of file +export default [interactionCreate]; diff --git a/src/structures/client.ts b/src/structures/client.ts index e7af2e1..dd363ab 100644 --- a/src/structures/client.ts +++ b/src/structures/client.ts @@ -1,13 +1,13 @@ import path from "path"; import { Collection, Client as DiscordClient, InteractionCollector, type ClientOptions } from "discord.js"; import type { YuukoComponent } from "#utils/types"; -import type { ClientCommand } from "./command"; +import type { Command } from "./command"; import Logger from "#utils/logger"; import { RSA } from "#utils/rsaEncryption"; import { Modules } from "./modules"; export class Client extends DiscordClient { - public commands: Collection; + public commands: Collection; public components: Collection; public cooldowns: Collection>; public modalData: Collection, won: boolean, guesses: number, hintsUsed: number }>>; @@ -18,7 +18,7 @@ export class Client extends DiscordClient { constructor(o: ClientOptions) { super(o); - this.logger = new Logger(path.join(import.meta.dir, "..", "Logging", "Logs.log")); + this.logger = new Logger(path.join(import.meta.dir, "..", "logging", "logs.json")); this.commands = new Collection(); this.components = new Collection(); this.cooldowns = new Collection(); @@ -26,8 +26,4 @@ export class Client extends DiscordClient { this.rsa = new RSA(); this.modules = new Modules(); } - - log(text: string, category: string) { - this.logger.log(text, category); - } } diff --git a/src/structures/command.ts b/src/structures/command.ts index f875375..1efaf97 100644 --- a/src/structures/command.ts +++ b/src/structures/command.ts @@ -35,17 +35,6 @@ export type UsableInteraction = | (ChatInputCommandInteraction & BaseExtension) | (ButtonInteraction & BaseExtension); -interface CommandStringOption { - name: string; - description: string; - required?: boolean; - type: ApplicationCommandOptionType; - choices?: { - name: string; - value: string; - }[]; -} - export type HookData = Partial<{ fields: APIEmbedField[]; title: string; @@ -53,16 +42,6 @@ export type HookData = Partial<{ image: string; }> -export type RunOptionsWithHooks = RunOptions & - Partial<{ - hook: boolean; - hookdata: HookData; - }>; - -export type CommonCommandWithHook = Omit & { - run: (o: RunOptionsWithHooks) => MaybePromise; -}; - export type CommandType = (typeof CommandCategories)[keyof typeof CommandCategories]; export interface Command { @@ -77,12 +56,4 @@ export interface Command { withBuilder: SlashCommandBuilder | SlashCommandOptionsOnlyBuilder | SlashCommandSubcommandsOnlyBuilder; run: (o: RunOptions, hookData?: hookData) => MaybePromise; -} - -export type CommandWithHook = CommonCommandWithHook & { - withBuilder?: any; -} - -export type ClientCommand = Command & { - options?: CommandStringOption[]; -} +} \ No newline at end of file diff --git a/src/structures/event.ts b/src/structures/event.ts index fb53396..4c5da0a 100644 --- a/src/structures/event.ts +++ b/src/structures/event.ts @@ -7,7 +7,6 @@ export type UsableClientEvents = DiscordClientEvents & { } export type ClientEvent = keyof UsableClientEvents - type YuukoEventRun = (client: Client, ...args: UsableClientEvents[Event]) => MaybePromise; interface YuukoEventOptions { event: Event diff --git a/src/utils/footer.ts b/src/utils/footer.ts index b028c99..69b86ed 100644 --- a/src/utils/footer.ts +++ b/src/utils/footer.ts @@ -1,14 +1,4 @@ -/** - * Returns the footer string. - * @param [headers=null] Optional HTTP headers to get the ratelimit values. - * @returns {object} The footer object. - */ -interface Headers { - [key: string]: string - 'x-ratelimit-remaining': string - 'x-ratelimit-limit': string -} -export function footer(headers?: Headers | null) { - const footerString = headers ? `Yuuko Beta ( ${`${headers['x-ratelimit-remaining'] || 0} / ${headers['x-ratelimit-limit'] || 0}`} )` : `Yuuko Beta` +export function footer(headers?: Bun.__internal.BunHeadersOverride | null) { + const footerString = headers ? `Yuuko Beta ( ${`${headers.get('x-ratelimit-remaining') || 0} / ${headers.get('x-ratelimit-limit') || 0}`} )` : `Yuuko Beta` return { text: footerString } } diff --git a/src/utils/getOption.ts b/src/utils/getOption.ts index 4a573cb..cc86810 100644 --- a/src/utils/getOption.ts +++ b/src/utils/getOption.ts @@ -1,57 +1,64 @@ import type { UsableInteraction } from "#structures/command"; import type { User } from "discord.js"; import { YuukoError } from "./types"; +import { client } from "#src/app"; export function getStringOption | undefined>(interaction: UsableInteraction, hookData: T, key: keyof NonNullable & string, required: true): string; export function getStringOption | undefined>(interaction: UsableInteraction, hookData: T, key: keyof NonNullable & string, required?: false): string | null; export function getStringOption | undefined>(interaction: UsableInteraction, hookData: T, key: keyof NonNullable & string, required = false) { - if (hookData && key in hookData && hookData[key] != null) { - return hookData[key as string]; - } + let returnValue; - if (interaction.isChatInputCommand?.()) { - return interaction.options.getString(key, required); - } + if (hookData && key in hookData && hookData[key] != null) { + returnValue = hookData[key as string]; + } else if (interaction.isChatInputCommand?.()) { + returnValue = interaction.options.getString(key, required); + } - if (required) { - throw new YuukoError(`Missing required option: ${key}`); - } + client.logger.debug("getStringOption", { type: "generic", key, required, value: returnValue }); - return null; + if (required && returnValue == null) { + throw new YuukoError(`Missing required option: ${key}`); + } + + return returnValue; } export function getUserOption | undefined>(interaction: UsableInteraction, hookData: T, key: keyof NonNullable & string, required: true): User; export function getUserOption | undefined>(interaction: UsableInteraction, hookData: T, key: keyof NonNullable & string, required?: false): User | null; export function getUserOption | undefined>(interaction: UsableInteraction, hookData: T, key: keyof NonNullable & string, required = false) { - if (hookData && key in hookData && hookData[key] != null) { - return hookData[key as string]; - } + let returnValue; + + if (hookData && key in hookData && hookData[key] != null) { + returnValue = hookData[key as string]; + } else if (interaction.isChatInputCommand?.()) { + returnValue = interaction.options.getUser(key, required); + } - if (interaction.isChatInputCommand?.()) { - return interaction.options.getUser(key, required); - } + client.logger.debug("getUserOption", { type: "generic", key, required, value: returnValue }); - if (required) { - throw new YuukoError(`Missing required user: ${key}`); - } + if (required && returnValue == null) { + throw new YuukoError(`Missing required user: ${key}`); + } - return null; + return returnValue; } export function getSubcommandOption | undefined>(interaction: UsableInteraction, hookData: T, key: keyof NonNullable & string, required: true): string; export function getSubcommandOption | undefined>(interaction: UsableInteraction, hookData: T, key: keyof NonNullable & string, required?: false): string | null; export function getSubcommandOption | undefined>(interaction: UsableInteraction, hookData: T, key: keyof NonNullable & string, required = false) { - if (hookData && key in hookData && hookData[key] != null) { - return hookData[key as string]; - } + let returnValue; + + if (hookData && key in hookData && hookData[key] != null) { + returnValue = hookData[key as string]; + } else if (interaction.isChatInputCommand?.()) { + returnValue = interaction.options.getSubcommand(required); + } - if (interaction.isChatInputCommand?.()) { - return interaction.options.getSubcommand(required); - } + client.logger.debug("getSubcommandOption", { type: "generic", key, required, value: returnValue }); - if (required) { - throw new YuukoError(`Missing required subcommand: ${key}`); - } + if (required && returnValue == null) { + throw new YuukoError(`Missing required subcommand: ${key}`); + } - return null; + return returnValue; } diff --git a/src/utils/graphQLRequest.ts b/src/utils/graphQLRequest.ts index 42ec340..c9835cb 100644 --- a/src/utils/graphQLRequest.ts +++ b/src/utils/graphQLRequest.ts @@ -43,6 +43,7 @@ import type { import Queries from '../graphQL/types/queries' import { YuukoError, type GraphQLResponse } from './types' import { env } from '#env'; +import { client } from '#src/app'; type Query = keyof typeof Queries @@ -93,14 +94,22 @@ export async function graphQLRequest(queryKey: QueryKey, if (!res.ok) { let errorMessage = ""; if (resJson.errors) errorMessage = resJson.errors[0].message; - throw new YuukoError(`${res.status} ${errorMessage} ${res.statusText}`, vars); + throw new YuukoError(`${res.status} ${errorMessage} ${res.statusText}`, { vars }); } const data = resJson as GraphQLResponse + client.logger.debug("GraphQL Request", { + type: "graphql", + query: queryKey, + vars, + authenticated: token !== undefined, + rateLimitRemaining: parseInt(res.headers.get("x-ratelimit-remaining") ?? ""), + }); + return { data: data.data, headers: res.headers }; } catch (e: any) { - console.error(e) - throw new YuukoError(e?.message || e, vars); + client.logger.error(e); + throw new YuukoError(e?.message || e, { vars }); } } diff --git a/src/utils/handleMediaData.ts b/src/utils/handleMediaData.ts index a05c134..7db0f4e 100644 --- a/src/utils/handleMediaData.ts +++ b/src/utils/handleMediaData.ts @@ -1,4 +1,4 @@ -import type { HookData, UsableInteraction } from "#structures/index"; +import type { Client, HookData, UsableInteraction } from "#structures/index"; import type { AnimeQuery, MangaQuery, Maybe, ScoreFormat } from "#graphQL/types"; import type { AlwaysExist, CacheEntry, GraphQLResponse } from "./types"; import { buildPagination, footer, SeriesTitle } from "."; @@ -14,6 +14,7 @@ export async function handleData( headers?: GraphQLResponse["headers"]; }, interaction: UsableInteraction, + client: Client, mediaType: "ANIME" | "MANGA", hookdata?: HookData | null, ) { @@ -175,20 +176,20 @@ export async function handleData( if (mediaUsers.length > 1) { const mediaPool = mediaUsers.map(async (user) => { const result = await redis.json.get(`_user${user.anilistId}-${media.id}`,).catch((e) => { - return console.log(e); + client.logger.error(e); + return; }); - if (!result) { - console.log(`No data found for user ${user.anilistId}`) - return null; - } + if (!result) return null; return result as CacheEntry }); const userData = (await Promise.all(mediaPool)).filter((u) => u != null); + client.logger.debug("Media user cached data", { type: "generic", total: userData.length, mediaId: media.id }) if (userData.every((e) => e == null)) return await buildPagination(interaction, pageList); + const statisticsEmbed = new EmbedBuilder() .setAuthor({ name: `${media.title?.english || media.title?.romaji || "N/A"} | Statistics for Yuuko Users!` }) .setImage(media.bannerImage!) @@ -209,7 +210,11 @@ function fixScoring(user: CacheEntry | null, scoreType: Maybe | und score = scoreValue.toString(); if (scoreType === "POINT_10_DECIMAL" || scoreType === "POINT_10") score = `${score} / 10`; else if (scoreType === "POINT_100" || scoreType === "POINT_5") score = `${score} / ${scoreType.split("POINT_")[1]}`; - else if (scoreType === "POINT_3") score = score === "1" ? "☹️" : score === "2" ? "😐" : "🙂"; + else if (scoreType === "POINT_3") { + if (scoreValue > 3) { + score = scoreValue >= 3.5 ? "☹️" : scoreValue >= 6 ? "😐" : "🙂"; + } else score = score === "0" ? "?" : score === "1" ? "☹️" : score === "2" ? "😐" : "🙂"; + } } else if (user && user.status) score = capitalize(user.status.toString()); return score; } diff --git a/src/utils/index.ts b/src/utils/index.ts index 0b9bc1c..953c69e 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -3,7 +3,6 @@ export * from "./commandCategories"; export * from "./embedError"; export * from "./footer"; export * from "./graphQLRequest"; -export * from "./logging"; export * from "./rsaEncryption"; export * from "./types"; export * from "./registerCommands"; diff --git a/src/utils/logger.ts b/src/utils/logger.ts index 2797ea0..5da47ae 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -1,35 +1,129 @@ -import winston, { config } from 'winston' -import { env } from '#env'; +import winston from "winston"; +import { env } from "#env"; +import type { Command, UsableInteraction } from "#structures/command"; + +export type LogLevel = "error" | "warn" | "info" | "http" | "verbose" | "debug" | "silly"; + +type LogMeta = GenericMeta | GraphQLMeta | CheckMeta | CommandMeta | EventMeta | StartupMeta | CommandDebugMeta; + +type GenericMeta = { + type: "generic", + command?: string; + subcommand?: string; + user?: string; + userId?: string; + guildId?: string; + [key: string]: unknown; +}; + +type CommandMeta = { + type: "command", + command: string; + subcommand?: string; + user: string; + userId: string; + guildId: string | null; +} + +type CommandDebugMeta = { + type: "commandDebug"; + command: string; + subcommand?: string; + [key: string]: unknown; +} + +type EventMeta = { + type: "event", + name: string, + isOnce: boolean, +} + +type GraphQLMeta = { + type: "graphql"; + query: string; + vars: Record, + durationMs?: number; + rateLimitRemaining?: number; + authenticated: boolean; +}; + +type CheckMeta = { + type: "check"; + name?: string, + optional?: boolean, + purpose?: string, + why?: unknown, + total?: number, +}; + +type StartupMeta = { + type: "startup"; + environment?: string; + user?: string; + total?: number; + commands?: string[]; + error?: string; + component?: string; +}; class Logger { public logger: winston.Logger; constructor(filename: string) { this.logger = winston.createLogger({ - transports: [new winston.transports.File({ filename })], - level: env().NODE_ENV === 'development' ? 'debug' : 'verbose', - format: winston.format.combine( - winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), - winston.format.printf(({ timestamp, level, message }) => { - return `${timestamp} | [${level.toUpperCase()}]: ${message}`; - }) - ) + level: "debug", + transports: [ + new winston.transports.File({ + filename, + format: winston.format.combine(winston.format.timestamp(), winston.format.json()), + }), + ], }); - this.logger.add(new winston.transports.Console({ - format: winston.format.combine( - winston.format.colorize({ level: true }), - winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), - winston.format.printf(({ timestamp, level, message }) => { - return `${timestamp} | [${level}]: ${message}`; - }) - ), - })); + this.logger.add( + new winston.transports.Console({ + level: env().NODE_ENV === "development" ? "debug" : "info", + format: winston.format.combine( + winston.format.colorize(), + winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), + winston.format.printf(({ timestamp, level, message, ...meta }) => { + const metaStr = Object.keys(meta).length ? JSON.stringify(meta) : ""; + return `${timestamp} | [${level}]: ${message} ${metaStr}`; + }), + ), + }), + ); + } + + log(level: LogLevel, message: string, meta?: LogMeta) { + this.logger.log(level, message, meta); } - log(text: string, category: string = "info") { - this.logger.log(category.toLowerCase(), text); + info(message: string, meta?: LogMeta) { + this.logger.info(message, meta); + } + + error(message: string, meta?: LogMeta) { + this.logger.error(message, meta); + } + + debug(message: string, meta?: LogMeta) { + this.logger.debug(message, meta); + } + + logCommand(command: Command, interaction: UsableInteraction) { + if (!interaction.isChatInputCommand()) return; + const subcommand = interaction.options.getSubcommand(false) ?? ""; + + this.debug("Command executed", { + type: "command", + command: command.name, + subcommand, + user: interaction.user.tag, + userId: interaction.user.id, + guildId: interaction.guildId, + }); } } -export default Logger +export default Logger; diff --git a/src/utils/logging.ts b/src/utils/logging.ts deleted file mode 100644 index dbd8577..0000000 --- a/src/utils/logging.ts +++ /dev/null @@ -1,27 +0,0 @@ -import fs from 'fs' -import path from 'path' -import type { Command, UsableInteraction } from '#structures/index' -import type { YuukoLog } from './types' - -export function logging(command: Command, interaction: UsableInteraction) { - try { - const logPath = path.join(import.meta.dir, '..', 'Logging/logs.json') - const currentDate = new Date().toISOString().replace(/T/, ' ').replace(/\..+/, '') - - if (!interaction.isCommand()) return - const subcommand = interaction.options.getSubcommand(false); - - const log = { - date: currentDate, - user: `${interaction.user.username}${+interaction.user.discriminator === 0 ? '' : "#" + interaction.user.discriminator}`, - info: `${command.name} ${subcommand}` - } satisfies YuukoLog - const previousLogs = JSON.parse(fs.readFileSync(logPath, 'utf8')) as Array - previousLogs.push(log) - - fs.writeFileSync(logPath, JSON.stringify(previousLogs)); - } - catch (e) { - console.log(e) - } -} diff --git a/src/utils/registerCommands.ts b/src/utils/registerCommands.ts index 711c501..71e0a50 100644 --- a/src/utils/registerCommands.ts +++ b/src/utils/registerCommands.ts @@ -5,22 +5,23 @@ import { REST, Routes } from "discord.js"; import { env } from '#env'; export async function registerCommands(client: Client) { - client.log(`Starting Yuuko in ${env().NODE_ENV} enviroment.`, "Info"); + client.logger.info("Starting bot", { type: "startup", environment: env().NODE_ENV }) + const commandsPath = path.join(import.meta.dir, "..", "commands"); const commandFiles = fs.readdirSync(commandsPath).filter((file) => file.endsWith(".ts")); - client.log(`Loading ${commandFiles.length} commands.`, "Info"); + + client.logger.info("Loaded commands", { type: "startup", total: commandFiles.length, commands: commandFiles }) + const slashCommands = commandFiles.map((file) => { const cmd = require(path.join(commandsPath, file)).default as Command; const builder = cmd?.withBuilder ?? {}; - delete cmd.withBuilder; - const data = { ...builder, ...cmd }; client.commands.set(data.name, data); return data; }); - client.log(`Loaded ${slashCommands.length} slash (/) commands.`, "Info"); + client.logger.info("Loaded slash commands", { type: "startup", total: slashCommands.length }) // ^ Register Slash Commands const rest = new REST({ version: "10" }).setToken(env().TOKEN!); @@ -29,16 +30,21 @@ export async function registerCommands(client: Client) { const guildId = env().GUILD_ID; try { - client.log(`Started refreshing ${slashCommands.length} slash (/) commands.`, "Info"); - - client.log(`Commands: ${slashCommands.map((x) => x.name).join(", ")}`, "Info"); + client.logger.info(`Started refreshing ${slashCommands.length} slash (/) commands.`, { + type: "startup", + commands: slashCommands.map((x) => x.name), + }); await rest.put(Routes.applicationGuildCommands(clientId, guildId), { body: slashCommands }); - if (env().NODE_ENV === "production" || env().NODE_ENV === "docker") await rest.put(Routes.applicationCommands(clientId), { body: slashCommands }); + if (env().NODE_ENV === "production" || env().NODE_ENV === "docker") + await rest.put(Routes.applicationCommands(clientId), { body: slashCommands }); - client.log(`Refreshed ${slashCommands.length} slash (/) commands.`, "Info"); + client.logger.info(`Refreshed ${slashCommands.length} slash (/) commands.`, { + type: "startup", + commands: slashCommands.map((x) => x.name), + }); } catch (error: any) { - client.log(error?.message ?? error, "Info"); + client.logger.error("Failed to refresh slash commands", { type: "startup", error: error?.message ?? error }); } } diff --git a/src/utils/registerComponents.ts b/src/utils/registerComponents.ts index 1361166..def4d7a 100644 --- a/src/utils/registerComponents.ts +++ b/src/utils/registerComponents.ts @@ -9,7 +9,7 @@ export async function registerComponents(client: Client) { .filter((file) => file.endsWith(".ts")) .forEach(file => { const component = require(path.join(compPath, file)).default as YuukoComponent; - client.log(`Component ${component.name} loaded`, "Info"); + client.logger.info("Component loaded", { type: "startup", component: component.name }); client.components.set(component.name, component); }); } diff --git a/src/utils/registerEvents.ts b/src/utils/registerEvents.ts index d325989..ce5c36e 100644 --- a/src/utils/registerEvents.ts +++ b/src/utils/registerEvents.ts @@ -1,12 +1,11 @@ import path from "path"; import fs from "fs"; -import { removeExtension } from "."; -import type { Client, YuukoEvent } from "#structures/index"; +import type { Client, ClientEvent, YuukoEvent } from "#structures/index"; export async function registerEvents(client: Client) { const eventsPath = path.join(import.meta.dir, "..", "events"); - const events: YuukoEvent[] = ( + const events: YuukoEvent[] = ( await Promise.all( fs .readdirSync(eventsPath) @@ -19,13 +18,16 @@ export async function registerEvents(client: Client) { ).flat(); for (const event of events) { - if (!event.run) { - client.log(`Event ${event.event} does not have a run function`, "error"); + client.logger.error("Event has no run function", { type: "generic", event: event.event }) process.exit(0); } client[event.isOnce ? "once" : "on"](event.event, (...args) => event.run(client, ...args)); - client.log(`Registered ${event.isOnce ? "once" : "on"}.${event.event}`, "info"); + client.logger.info(`Registered event listener`, { + type: "event", + name: event.event, + isOnce: event.isOnce + }); } } diff --git a/src/utils/types.ts b/src/utils/types.ts index 7e0931d..180f7f2 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -55,19 +55,19 @@ export type YuukoLog = { } export class YuukoError extends Error { - vars?: any + vars?: Record ephemeral?: boolean cause?: string - constructor(message: string, vars?: any, ephemeral: boolean = false, cause?: string) { + constructor(message: string, options?: { vars?: Record, ephemeral?: boolean, cause?: string }) { super(message); this.name = 'YuukoError'; this.message = message; - this.vars = vars; - this.ephemeral = ephemeral; - this.cause = cause; + this.vars = options?.vars; + this.ephemeral = options?.ephemeral; + this.cause = options?.cause; Object.setPrototypeOf(this, YuukoError.prototype); } -} \ No newline at end of file +} diff --git a/src/workers/manager.ts b/src/workers/manager.ts index 6a2ac69..0514546 100644 --- a/src/workers/manager.ts +++ b/src/workers/manager.ts @@ -2,6 +2,8 @@ declare var self: Worker; import { db, tables, type InferTable } from '#database/index'; +import { env } from '#src/env'; +import type { LogLevel } from '#utils/logger'; import { RSA } from "#utils/rsaEncryption"; import { eq } from 'drizzle-orm'; @@ -17,7 +19,7 @@ export type ReminderMessage = { export type LogMessage = { type: 'LOG'; text: string; - category: string; + level: LogLevel; }; export type SyncUsers = { @@ -34,6 +36,8 @@ async function checkUpcomingEpisodes() { } async function updateSyncedUsers() { + if (env().NODE_ENV === "development") return; + try { const syncEvent = (await db.select().from(tables.workerEvents).where(eq(tables.workerEvents.type, "SYNC")).limit(1))[0]; @@ -45,12 +49,6 @@ async function updateSyncedUsers() { if (updateAt > currentDate) return; - self.postMessage({ - type: 'LOG', - text: `Preparing to sync all users...`, - category: 'verbose', - } satisfies LogMessage); - const anilistUsers = await db.select().from(tables.anilistUser); const rsa = new RSA(); diff --git a/src/workers/syncUsers.ts b/src/workers/syncUsers.ts index 4514ed0..ec27876 100644 --- a/src/workers/syncUsers.ts +++ b/src/workers/syncUsers.ts @@ -3,7 +3,7 @@ import { db, tables } from "#database/index"; import { MediaType } from "#graphQL/types"; import { decodeJWT, graphQLRequest, YuukoError } from "#utils/index"; import type { SyncUsers } from "#workers/manager"; -import { client } from "app"; +import { client } from "#src/app"; import { eq } from "drizzle-orm"; // for main thread to call @@ -13,7 +13,7 @@ export async function syncAnilistUsers(data: SyncUsers) { const total = anilistUsers.length; - client.log(`Preparing to sync a total of ${anilistUsers.length} users.`, "verbose"); + client.logger.log("verbose", "Preparing to sync users", { type: "generic", total }); const date = Math.floor(Date.now() / 1000); @@ -32,14 +32,14 @@ export async function syncAnilistUsers(data: SyncUsers) { const remaining = parseInt(animeHeaders.get("x-ratelimit-remaining") ?? "1"); if (remaining <= 2) { - client.log(`Rate limit nearly exhausted (${remaining} remaining), pausing for 60s...`, "warn"); + client.logger.log("warn", "Rate limit nearly exhausted, waiting 60s", { type: "generic", remaining }); await new Promise((resolve) => setTimeout(resolve, 60 * 1000)); } const { data: mangaData } = await graphQLRequest("GetUserList", { userId: user.anilistId, type: MediaType.Manga }, user.anilistToken); if (mangaData) await handleSyncing({ media: mangaData }, user.anilistId, MediaType.Manga); - client.log(`Synced user ${user.anilistId} (${i + 1} / ${total})`, "verbose"); + client.logger.log("verbose", "Synced user", { type: "generic", anilistId: user.anilistId, idx: i + 1, total }); const localTimeout = Math.max(0, Math.floor(timeOut - (performance.now() - start))); if (i < total - 1 && localTimeout > 0) { @@ -58,7 +58,7 @@ export async function syncAnilistUsers(data: SyncUsers) { } async function deleteUser(anilistId: number) { - client.log(`User ${anilistId} has an invalid token, deleting from the DB...`, "verbose"); + client.logger.log("warn", "User has invalid token, deleting from DB", { type: "generic", anilistId }); await db .delete(tables.anilistUser) .where(eq(tables.anilistUser.anilistId, anilistId)); diff --git a/tsconfig.json b/tsconfig.json index e2128a5..e75a722 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,78 +1,41 @@ { - "compilerOptions": { - "target": "es2022", - "lib": [ - "es2022" - ], - "moduleDetection": "force", - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "module": "ESNext", - "moduleResolution": "Bundler", - "resolveJsonModule": true, - "types": [ - "bun-types" // add Bun global - ], - "allowJs": true, - "strict": true, - "noUncheckedIndexedAccess": true, - "noEmit": true, - "esModuleInterop": true, - "verbatimModuleSyntax": true, - "skipLibCheck": true, - "baseUrl": "src", - "paths": { - "#api/*": [ - "api/*" - ], - "#caching/*": [ - "caching/*" - ], - "#checks/*": [ - "checks/*" - ], - "#commands/*": [ - "commands/*" - ], - "#components/*": [ - "components/*" - ], - "#database/*": [ - "database/*" - ], - "#events/*": [ - "events/*" - ], - "#graphQL/*": [ - "graphQL/*" - ], - "#logging/*": [ - "Logging/*" - ], - "#middleware/*": [ - "middleware/*" - ], - "#models/*": [ - "database/models/*" - ], - "#RSA/*": [ - "RSA/*" - ], - "#scripts/*": [ - "scripts/*" - ], - "#structures/*": [ - "structures/*" - ], - "#utils/*": [ - "utils/*" - ], - "#workers/*": [ - "./workers/*" - ], - "#env": [ - "env.ts" - ] - }, - } -} \ No newline at end of file + "compilerOptions": { + "target": "es2022", + "lib": ["es2022"], + "moduleDetection": "force", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "types": [ + "bun-types" // add Bun global + ], + "allowJs": true, + "strict": true, + "noUncheckedIndexedAccess": true, + "noEmit": true, + "esModuleInterop": true, + "verbatimModuleSyntax": true, + "skipLibCheck": true, + "paths": { + "#src/*": ["./src/*"], + "#api/*": ["./src/api/*"], + "#caching/*": ["./src/caching/*"], + "#checks/*": ["./src/checks/*"], + "#commands/*": ["./src/commands/*"], + "#components/*": ["./src/components/*"], + "#database/*": ["./src/database/*"], + "#events/*": ["./src/events/*"], + "#graphQL/*": ["./src/graphQL/*"], + "#middleware/*": ["./src/middleware/*"], + "#models/*": ["./src/database/models/*"], + "#RSA/*": ["./src/RSA/*"], + "#scripts/*": ["./src/scripts/*"], + "#structures/*": ["./src/structures/*"], + "#utils/*": ["./src/utils/*"], + "#workers/*": ["./src/workers/*"], + "#env": ["./src/env.ts"] + } + } +}