diff --git a/package-lock.json b/package-lock.json index 40acd8c..3d0a388 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@discordjs/rest": "^0.5.0", "@ghom/handler": "^3.1.0", - "@ghom/logger": "^2.0.0", + "@ghom/logger": "^2.0.1", "@ghom/orm": "^1.7.0", "boxen": "^7.0.0", "chalk": "^5.0.1", @@ -29,7 +29,8 @@ "sqlite3": "^5.0.8", "tims": "^2.1.0", "types-package-json": "^2.0.39", - "yargs-parser": "^21.0.1" + "yargs-parser": "^21.0.1", + "zod": "^3.23.8" }, "devDependencies": { "@esbuild/linux-x64": "^0.20.1", @@ -983,9 +984,9 @@ } }, "node_modules/@ghom/logger": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@ghom/logger/-/logger-2.0.0.tgz", - "integrity": "sha512-yjlx3XxqDWODT3UnC0ZDbUX5w5uLKTxTLy0Hk0X8a0D7MhImAqGdAaSv2ByrfowwZuckfbZeEEiCYcBFp3Q57A==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@ghom/logger/-/logger-2.0.1.tgz", + "integrity": "sha512-PBNXbQAS1H2nO9+sCnJ4jmnUVrsVBatbejcLD9h8827F4Wl0k/JpeLBITb990AsZlplE9EWHAwmjjdaN1whMTg==", "dependencies": { "chalk": "=4.1.2", "dayjs": "^1.11.7" @@ -11453,6 +11454,14 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index e016819..b9237ce 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "dependencies": { "@discordjs/rest": "^0.5.0", "@ghom/handler": "^3.1.0", - "@ghom/logger": "^2.0.0", + "@ghom/logger": "^2.0.1", "@ghom/orm": "^1.7.0", "boxen": "^7.0.0", "chalk": "^5.0.1", @@ -56,7 +56,8 @@ "sqlite3": "^5.0.8", "tims": "^2.1.0", "types-package-json": "^2.0.39", - "yargs-parser": "^21.0.1" + "yargs-parser": "^21.0.1", + "zod": "^3.23.8" }, "devDependencies": { "@esbuild/linux-x64": "^0.20.1", diff --git a/src/app.native.ts b/src/app.native.ts index b051824..5daf806 100644 --- a/src/app.native.ts +++ b/src/app.native.ts @@ -18,3 +18,5 @@ export * as command from "./app/command.ts" export * as logger from "./app/logger.ts" export * as slash from "./app/slash.ts" export * as util from "./app/util.ts" + +export { default as env } from "./app/env.ts" diff --git a/src/app/command.ts b/src/app/command.ts index dd9afd8..95554fc 100644 --- a/src/app/command.ts +++ b/src/app/command.ts @@ -9,13 +9,14 @@ import yargsParser from "yargs-parser" import * as handler from "@ghom/handler" +import env from "./env.ts" + import * as util from "./util.ts" import * as logger from "./logger.ts" import * as argument from "./argument.ts" import * as config from "./config.ts" import { filename } from "dirname-filename-esm" - const __filename = filename(import.meta) export const commandHandler = new handler.Handler( @@ -476,7 +477,7 @@ export async function prepareCommand( if (util.scrap(cmd.options.guildOwnerOnly, message)) if ( message.guild.ownerId !== message.member.id && - process.env.BOT_OWNER !== message.member.id + env.BOT_OWNER !== message.member.id ) return util.getSystemMessage("error", { author: { @@ -569,7 +570,7 @@ export async function prepareCommand( }) if (await util.scrap(cmd.options.botOwnerOnly, message)) - if (process.env.BOT_OWNER !== message.author.id) + if (env.BOT_OWNER !== message.author.id) return util.getSystemMessage("error", { author: { name: "You must be my owner.", diff --git a/src/app/database.ts b/src/app/database.ts index 9b8624c..ce6fed2 100644 --- a/src/app/database.ts +++ b/src/app/database.ts @@ -2,6 +2,7 @@ import { ORM } from "@ghom/orm" import { logger } from "@ghom/logger" +import env from "./env.ts" import path from "path" import fs from "fs" @@ -16,7 +17,7 @@ export const orm = new ORM({ useNullAsDefault: true, connection: { filename: path.join(dataDirectory, "sqlite3.db"), - timezone: process.env.BOT_TIMEZONE || "UTC", + timezone: env.BOT_TIMEZONE || "UTC", }, }, logger, diff --git a/src/app/env.ts b/src/app/env.ts new file mode 100644 index 0000000..7cb038a --- /dev/null +++ b/src/app/env.ts @@ -0,0 +1,82 @@ +import "dotenv/config" + +import { z, ZodError } from "zod" +import fs from "fs" +import path from "path" +import chalk from "chalk" + +import * as logger from "./logger.ts" + +const localeList: { key: string; name: string }[] = JSON.parse( + fs.readFileSync( + path.join(process.cwd(), "node_modules", "dayjs", "locale.json"), + "utf-8", + ), +) + +const localeKeys = localeList.map((locale) => locale.key) as [ + string, + ...string[], +] + +const timezones = Intl.supportedValuesOf("timeZone") as [string, ...string[]] + +const envSchema = z.object({ + BOT_TOKEN: z.string({ + message: `You need to add your ${chalk.bold("BOT_TOKEN")} in the .env file. You can found this token on this page in the "Bot" tab: ${ + process.env.BOT_ID + ? `https://discord.com/developers/applications/${process.env.BOT_ID}/information` + : "https://discord.com/developers/applications" + }`, + }), + BOT_PREFIX: z.string({ + message: `You need to add a ${chalk.bold("BOT_PREFIX")} in the .env file, for example: BOT_PREFIX="!"`, + }), + BOT_OWNER: z.string({ + message: `You need to add your ${chalk.bold("BOT_OWNER")} Discord ID in the .env file, for example: BOT_OWNER="123456789012345678"`, + }), + BOT_ID: z.string({ + message: `You need to add the ${chalk.bold("BOT_ID")} in the .env file. You can found this ID on this page in the "General Information" tab: ${ + process.env.BOT_ID + ? `https://discord.com/developers/applications/${process.env.BOT_ID}/information` + : "https://discord.com/developers/applications" + }`, + }), + BOT_MODE: z.enum(["factory", "test", "development", "production"], { + message: `You need to add a ${chalk.bold("BOT_MODE")} in the .env file, for example: BOT_MODE="development"`, + }), + BOT_LOCALE: z + .enum(localeKeys, { + message: `Your ${chalk.bold("BOT_LOCALE")} in the .env file is not valid. You can choose between this list:\n=> ${localeKeys.join(", ")}`, + }) + .optional(), + BOT_TIMEZONE: z + .enum(timezones, { + message: `Your ${chalk.bold("BOT_TIMEZONE")} in the .env file is not valid. You can choose between this list:\n=> ${timezones.join(", ")}`, + }) + .optional(), + DB_PORT: z.coerce + .number({ + message: `The ${chalk.bold("DB_PORT")} must be a valid port number between 0 and 65535`, + }) + .max( + 65535, + `The ${chalk.bold("DB_PORT")} must be a valid port number between 0 and 65535`, + ) + .optional(), + DB_HOST: z.string().optional(), + DB_USER: z.string().optional(), + DB_PASSWORD: z.string().optional(), + DB_DATABASE: z.string().optional(), +}) + +let env: z.infer +try { + env = envSchema.parse(process.env) +} catch (error) { + const { errors } = error as ZodError + errors.forEach((err) => logger.error(err.message, ".env")) + process.exit(1) +} + +export default env diff --git a/src/app/pagination.ts b/src/app/pagination.ts index f4a41ba..906a9e3 100644 --- a/src/app/pagination.ts +++ b/src/app/pagination.ts @@ -7,7 +7,6 @@ import * as config from "./config.ts" import * as util from "./util.ts" import { filename } from "dirname-filename-esm" - const __filename = filename(import.meta) export type PaginatorKey = "previous" | "next" | "start" | "end" diff --git a/src/app/slash.ts b/src/app/slash.ts index 33997a8..a84b526 100644 --- a/src/app/slash.ts +++ b/src/app/slash.ts @@ -8,6 +8,9 @@ import path from "path" import chalk from "chalk" import * as handler from "@ghom/handler" + +import env from "./env.ts" + import * as logger from "./logger.ts" import * as config from "./config.ts" import * as util from "./util.ts" @@ -253,13 +256,13 @@ export function validateSlashCommand(command: ISlashCommand) { } export async function registerSlashCommands(guildId?: string) { - const api = new rest.REST({ version: "10" }).setToken(process.env.BOT_TOKEN!) + const api = new rest.REST({ version: "10" }).setToken(env.BOT_TOKEN) try { const data = (await api.put( guildId - ? v10.Routes.applicationGuildCommands(process.env.BOT_ID!, guildId) - : v10.Routes.applicationCommands(process.env.BOT_ID!), + ? v10.Routes.applicationGuildCommands(env.BOT_ID, guildId) + : v10.Routes.applicationCommands(env.BOT_ID), { body: slashCommands.map((cmd) => cmd.builder.toJSON()), }, @@ -287,10 +290,7 @@ export async function prepareSlashCommand( options: {}, } - if ( - command.options.botOwnerOnly && - interaction.user.id !== process.env.BOT_OWNER - ) + if (command.options.botOwnerOnly && interaction.user.id !== env.BOT_OWNER) return new discord.EmbedBuilder() .setColor("Red") .setDescription("This command can only be used by the bot owner") diff --git a/src/app/util.ts b/src/app/util.ts index 439acba..cf32a3d 100644 --- a/src/app/util.ts +++ b/src/app/util.ts @@ -18,6 +18,8 @@ import relative from "dayjs/plugin/relativeTime.js" import timezone from "dayjs/plugin/timezone.js" import toObject from "dayjs/plugin/toObject.js" +import env from "./env.ts" + import * as logger from "./logger.ts" import * as config from "./config.ts" import * as client from "./client.ts" @@ -84,7 +86,7 @@ export async function checkUpdates() { } } -const locale = process.env.BOT_LOCALE +const locale = env.BOT_LOCALE import(`dayjs/locale/${locale ?? "en"}.js`) .then(() => dayjs.locale(locale ?? "en")) @@ -102,7 +104,7 @@ dayjs.extend(timezone) dayjs.extend(toObject) dayjs.utc(1) -if (process.env.BOT_TIMEZONE) dayjs.tz.setDefault(process.env.BOT_TIMEZONE) +if (env.BOT_TIMEZONE) dayjs.tz.setDefault(env.BOT_TIMEZONE) export { dayjs } diff --git a/src/commands/info.native.ts b/src/commands/info.native.ts index d76598b..e2c42da 100644 --- a/src/commands/info.native.ts +++ b/src/commands/info.native.ts @@ -34,7 +34,7 @@ export default new app.Command({ lang: "yml", content: [ `author: ${ - message.client.users.resolve(process.env.BOT_OWNER!)!.username + message.client.users.resolve(app.env.BOT_OWNER)!.username }`, `uptime: ${time.duration(app.uptime(), { format: "second", diff --git a/src/config.ts b/src/config.ts index 8c8dace..5cedd65 100644 --- a/src/config.ts +++ b/src/config.ts @@ -3,7 +3,7 @@ import * as app from "#app" export const config: app.Config = { ignoreBots: true, getPrefix() { - return process.env.BOT_PREFIX! + return app.env.BOT_PREFIX! }, client: { intents: [ diff --git a/src/index.ts b/src/index.ts index 4dc3166..6a2f320 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,14 +1,7 @@ import { filename } from "dirname-filename-esm" - const __filename = filename(import.meta) -import "dotenv/config.js" - -for (const key of ["BOT_TOKEN", "BOT_PREFIX", "BOT_OWNER", "BOT_ID"]) { - if (!process.env[key] || /^{{.+}}$/.test(process.env[key] as string)) { - throw new Error(`You need to add "${key}" value in your .env file.`) - } -} +import env from "./app/env.ts" const app = await import("#app") @@ -19,7 +12,7 @@ try { await app.listenerHandler.init() await app.initPagination() await app.checkUpdates() - await app.ClientSingleton.get().login(process.env.BOT_TOKEN) + await app.ClientSingleton.get().login(env.BOT_TOKEN) } catch (error: any) { app.error(error, __filename, true) process.exit(1) diff --git a/src/listeners/command.messageCreate.native.ts b/src/listeners/command.messageCreate.native.ts index 453859b..75dd38b 100644 --- a/src/listeners/command.messageCreate.native.ts +++ b/src/listeners/command.messageCreate.native.ts @@ -25,7 +25,7 @@ const listener: app.Listener<"messageCreate"> = { .catch() message.usedAsDefault = false - message.isFromBotOwner = message.author.id === process.env.BOT_OWNER! + message.isFromBotOwner = message.author.id === app.env.BOT_OWNER app.emitMessage(message.channel, message) app.emitMessage(message.author, message) @@ -61,7 +61,7 @@ const listener: app.Listener<"messageCreate"> = { if ( key !== "turn" && !app.cache.ensure("turn", true) && - message.author.id !== process.env.BOT_OWNER + message.author.id !== app.env.BOT_OWNER ) return diff --git a/src/listeners/log.afterReady.native.ts b/src/listeners/log.afterReady.native.ts index 3e5d462..acd7504 100644 --- a/src/listeners/log.afterReady.native.ts +++ b/src/listeners/log.afterReady.native.ts @@ -18,7 +18,7 @@ const listener: app.Listener<"afterReady"> = { app.log( `ok i'm ready! ${chalk.blue( "My default prefix is", - )} ${chalk.bgBlueBright.black(process.env.BOT_PREFIX)}`, + )} ${chalk.bgBlueBright.black(app.env.BOT_PREFIX)}`, ) figlet(app.packageJSON.name, (err, value) => { diff --git a/template.env b/template.env index 88e8061..48293bb 100644 --- a/template.env +++ b/template.env @@ -1,6 +1,3 @@ -# The Secret value of Discord Application -BOT_SECRET= - # The token of the bot BOT_TOKEN= diff --git a/templates/mysql2 b/templates/mysql2 index f9dd985..d87ad9d 100644 --- a/templates/mysql2 +++ b/templates/mysql2 @@ -2,6 +2,7 @@ import { ORM } from "@ghom/orm" import { logger } from "@ghom/logger" +import env from "./env.ts" import path from "path" export const orm = new ORM({ diff --git a/templates/pg b/templates/pg index 694afc3..0994f9c 100644 --- a/templates/pg +++ b/templates/pg @@ -2,6 +2,7 @@ import { ORM } from "@ghom/orm" import { logger } from "@ghom/logger" +import env from "./env.ts" import path from "path" export const orm = new ORM({ diff --git a/templates/sqlite3 b/templates/sqlite3 index 0eaee72..9107031 100644 --- a/templates/sqlite3 +++ b/templates/sqlite3 @@ -2,6 +2,7 @@ import { ORM } from "@ghom/orm" import { logger } from "@ghom/logger" +import env from "./env.ts" import path from "path" import fs from "fs"