From fa69a708f909046b5f25d9e035a9c724c9dc20eb Mon Sep 17 00:00:00 2001 From: deptyped Date: Thu, 9 Nov 2023 06:21:58 +0200 Subject: [PATCH] Use `znv` to load config --- .env.example | 9 +- README.md | 109 +++++++++++------- package-lock.json | 12 ++ package.json | 3 +- scripts/start.ts | 4 +- src/bot/features/{bot-admin.ts => admin.ts} | 6 +- src/bot/features/index.ts | 2 +- src/bot/filters/index.ts | 2 +- .../filters/{is-bot-admin.ts => is-admin.ts} | 2 +- src/bot/handlers/commands/setcommands.ts | 2 +- src/bot/index.ts | 4 +- src/config.ts | 79 ++++++------- 12 files changed, 131 insertions(+), 103 deletions(-) rename src/bot/features/{bot-admin.ts => admin.ts} (72%) rename src/bot/filters/{is-bot-admin.ts => is-admin.ts} (56%) diff --git a/.env.example b/.env.example index d60f5d85..c457529a 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,7 @@ NODE_ENV=development +LOG_LEVEL=debug BOT_TOKEN=123:ABCABCD BOT_WEBHOOK=https://www.example.com/ -LOG_LEVEL=debug -BOT_SERVER_HOST=0.0.0.0 -BOT_SERVER_PORT=80 -BOT_ALLOWED_UPDATES=[] -BOT_ADMIN_USER_ID=1 \ No newline at end of file +BOT_SERVER_HOST=localhost +BOT_SERVER_PORT=3000 +BOT_ADMINS=[1] \ No newline at end of file diff --git a/README.md b/README.md index 92f71c93..4973d445 100644 --- a/README.md +++ b/README.md @@ -26,49 +26,62 @@ Bot starter template based on [grammY](https://grammy.dev/) bot framework. ## Usage -1. [Create a new repository](https://github.com/bot-base/telegram-bot-template/generate) using this template. +Follow these steps to set up and run your bot using this template: -2. Create an environment variables file: +1. **Create a New Repository** -```bash -cp .env.example .env -``` - -3. Set BOT_TOKEN [environment variable](#environment-variables) in `.env` file. + Start by creating a new repository using this template. You can do this by clicking [here](https://github.com/bot-base/telegram-bot-template/generate). +2. **Environment Variables Setup** + + Create an environment variables file by copying the provided example file: + ```bash + cp .env.example .env + ``` + Open the newly created `.env` file and set the `BOT_TOKEN` environment variable. -4. Launch bot - - Development mode: +3. **Launching the Bot** + + You can run your bot in both development and production modes. + **Development Mode:** + + Install the required dependencies: + ```bash + npm install + ``` + Start the bot in watch mode (auto-reload when code changes): ```bash - # 1. Install dependencies - npm i - - # 2. Run bot (in watch mode) npm run dev ``` - Production mode: - + **Production Mode:** + + Install only production dependencies (no development dependencies): ```bash - # 1. Install dependencies - npm i --only=prod - - # 2. Set NODE_ENV to production and change BOT_WEBHOOK to the actual URL to receive updates - - # 3. Run bot - npm start + npm install --only=prod + ``` + + Set the `NODE_ENV` environment variable to "production" in your `.env` file. Also, make sure to update `BOT_WEBHOOK` with the actual URL where your bot will receive updates. + ```dotenv + NODE_ENV=production + BOT_WEBHOOK= + ``` + + Start the bot in production mode: + ```bash + npm start # or npm run start:force # if you want to skip type checking ``` -### List of available commands +### List of Available Commands + - `npm run lint` — Lint source code. - `npm run format` — Format source code. -- `npm run typecheck` — Runs type checking. -- `npm run dev` — Starts the bot in development mode. -- `npm run start` — Starts the bot. +- `npm run typecheck` — Run type checking. +- `npm run dev` — Start the bot in development mode. +- `npm run start` — Start the bot. - `npm run start:force` — Starts the bot without type checking. ## Deploy @@ -236,7 +249,7 @@ git merge template/example/webapp-vue -X theirs --squash --no-commit --allow-unr NODE_ENV String - Application environment (development or production) + Specifies the application environment. (development or production) BOT_TOKEN @@ -244,28 +257,40 @@ git merge template/example/webapp-vue -X theirs --squash --no-commit --allow-unr String - Token, get it from @BotFather. + Telegram Bot API token obtained from @BotFather. - - BOT_WEBHOOK + + LOG_LEVEL String - Webhook endpoint, used to configure webhook in production environment. + Optional. + Specifies the application log level.
+ For example, use info for general logging. View the Pino documentation for more log level options.
+ Defaults to info. - LOG_LEVEL + BOT_MODE String Optional. - Application log level. - See Pino docs for a complete list of available log levels.
- Defaults to info. + Specifies method to receive incoming updates. (polling or webhook) + Defaults to polling. + + + + BOT_WEBHOOK + + String + + + Optional in polling mode. + Webhook endpoint URL, used to configure webhook in production environment. @@ -274,7 +299,7 @@ git merge template/example/webapp-vue -X theirs --squash --no-commit --allow-unr String - Optional. Server address.
+ Optional. Specifies the server hostname.
Defaults to 0.0.0.0. @@ -284,7 +309,7 @@ git merge template/example/webapp-vue -X theirs --squash --no-commit --allow-unr Number - Optional. Server port.
+ Optional. Specifies the server port.
Defaults to 80. @@ -299,12 +324,14 @@ git merge template/example/webapp-vue -X theirs --squash --no-commit --allow-unr - BOT_ADMIN_USER_ID + BOT_ADMINS - Number or
Array of Number + Array of Number - Optional. Administrator user ID. Commands such as /setcommands will only be available to a user with this ID.
+ Optional. + Administrator user IDs. + Use this to specify user IDs that have special privileges, such as executing /setcommands.
Defaults to an empty array. diff --git a/package-lock.json b/package-lock.json index 10aee828..eaaca234 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "pino": "8.16.1", "pino-pretty": "10.2.3", "tsx": "3.14.0", + "znv": "^0.4.0", "zod": "3.22.4" }, "devDependencies": { @@ -5770,6 +5771,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/znv": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/znv/-/znv-0.4.0.tgz", + "integrity": "sha512-6/pGsQhBisLzKdyC90mUCRgYDtCfQ4aQ68sDybexq3GMzqqkp662GH6qIyuCHJC1i72hJPHbWAhccTJVuZUQfA==", + "dependencies": { + "colorette": "^2.0.19" + }, + "peerDependencies": { + "zod": "^3.13.2" + } + }, "node_modules/zod": { "version": "3.22.4", "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", diff --git a/package.json b/package.json index 72283845..21e062ab 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "pino": "8.16.1", "pino-pretty": "10.2.3", "tsx": "3.14.0", + "znv": "0.4.0", "zod": "3.22.4" }, "devDependencies": { @@ -62,4 +63,4 @@ "lint-staged": { "*.ts": "npm run lint" } -} \ No newline at end of file +} diff --git a/scripts/start.ts b/scripts/start.ts index 24b82170..068d4d95 100644 --- a/scripts/start.ts +++ b/scripts/start.ts @@ -18,7 +18,7 @@ try { await bot.stop(); }); - if (config.isProd) { + if (config.BOT_MODE === "webhook") { // to prevent receiving updates before the bot is ready await bot.init(); @@ -30,7 +30,7 @@ try { await bot.api.setWebhook(config.BOT_WEBHOOK, { allowed_updates: config.BOT_ALLOWED_UPDATES, }); - } else if (config.isDev) { + } else if (config.BOT_MODE === "polling") { await bot.start({ allowed_updates: config.BOT_ALLOWED_UPDATES, onStart: ({ username }) => diff --git a/src/bot/features/bot-admin.ts b/src/bot/features/admin.ts similarity index 72% rename from src/bot/features/bot-admin.ts rename to src/bot/features/admin.ts index 384fdbae..142d6929 100644 --- a/src/bot/features/bot-admin.ts +++ b/src/bot/features/admin.ts @@ -1,13 +1,13 @@ import { chatAction } from "@grammyjs/auto-chat-action"; import { Composer } from "grammy"; import type { Context } from "#root/bot/context.js"; -import { isBotAdmin } from "#root/bot/filters/index.js"; +import { isAdmin } from "#root/bot/filters/index.js"; import { setCommandsHandler } from "#root/bot/handlers/index.js"; import { logHandle } from "#root/bot/helpers/logging.js"; const composer = new Composer(); -const feature = composer.chatType("private").filter(isBotAdmin); +const feature = composer.chatType("private").filter(isAdmin); feature.command( "setcommands", @@ -16,4 +16,4 @@ feature.command( setCommandsHandler, ); -export { composer as botAdminFeature }; +export { composer as adminFeature }; diff --git a/src/bot/features/index.ts b/src/bot/features/index.ts index 4912038c..77f2609e 100644 --- a/src/bot/features/index.ts +++ b/src/bot/features/index.ts @@ -1,4 +1,4 @@ -export * from "./bot-admin.js"; +export * from "./admin.js"; export * from "./language.js"; export * from "./unhandled.js"; export * from "./welcome.js"; diff --git a/src/bot/filters/index.ts b/src/bot/filters/index.ts index c65c9602..f699fb48 100644 --- a/src/bot/filters/index.ts +++ b/src/bot/filters/index.ts @@ -1 +1 @@ -export * from "./is-bot-admin.js"; +export * from "./is-admin.js"; diff --git a/src/bot/filters/is-bot-admin.ts b/src/bot/filters/is-admin.ts similarity index 56% rename from src/bot/filters/is-bot-admin.ts rename to src/bot/filters/is-admin.ts index 072459cf..a2a3e1f2 100644 --- a/src/bot/filters/is-bot-admin.ts +++ b/src/bot/filters/is-admin.ts @@ -1,4 +1,4 @@ import { isUserHasId } from "grammy-guard"; import { config } from "#root/config.js"; -export const isBotAdmin = isUserHasId(...config.BOT_ADMIN_USER_ID); +export const isAdmin = isUserHasId(...config.BOT_ADMINS); diff --git a/src/bot/handlers/commands/setcommands.ts b/src/bot/handlers/commands/setcommands.ts index fb0d0517..ee2be2e2 100644 --- a/src/bot/handlers/commands/setcommands.ts +++ b/src/bot/handlers/commands/setcommands.ts @@ -101,7 +101,7 @@ export async function setCommandsHandler(ctx: CommandContext) { { scope: { type: "chat", - chat_id: Number(config.BOT_ADMIN_USER_ID), + chat_id: Number(config.BOT_ADMINS), }, }, ); diff --git a/src/bot/index.ts b/src/bot/index.ts index 74e1f53e..653285e7 100644 --- a/src/bot/index.ts +++ b/src/bot/index.ts @@ -8,7 +8,7 @@ import { createContextConstructor, } from "#root/bot/context.js"; import { - botAdminFeature, + adminFeature, languageFeature, unhandledFeature, welcomeFeature, @@ -52,7 +52,7 @@ export function createBot(token: string, options: Options = {}) { // Handlers protectedBot.use(welcomeFeature); - protectedBot.use(botAdminFeature); + protectedBot.use(adminFeature); if (isMultipleLocales) { protectedBot.use(languageFeature); diff --git a/src/config.ts b/src/config.ts index ab394554..b4dd4040 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,50 +1,39 @@ import "dotenv/config"; -import z, { ZodError, ZodIssueCode } from "zod"; +import z from "zod"; +import { parseEnv, port } from "znv"; import { API_CONSTANTS } from "grammy"; -function parseJsonSafe(path: string) { - return (value: unknown) => { - try { - return JSON.parse(String(value)); - } catch { - throw new ZodError([ - { - code: ZodIssueCode.custom, - path: [path], - fatal: true, - message: "Invalid JSON", - }, - ]); - } - }; -} - -const configSchema = z.object({ - NODE_ENV: z.enum(["development", "production"]), - LOG_LEVEL: z - .enum(["trace", "debug", "info", "warn", "error", "fatal", "silent"]) - .default("info"), - BOT_SERVER_HOST: z.string().default("0.0.0.0"), - BOT_SERVER_PORT: z.coerce.number().positive().default(80), - BOT_ALLOWED_UPDATES: z - .preprocess( - parseJsonSafe("BOT_ALLOWED_UPDATES"), - z.array(z.enum(API_CONSTANTS.ALL_UPDATE_TYPES)), - ) - .default([]), - BOT_TOKEN: z.string(), - BOT_WEBHOOK: z.string().url(), - BOT_ADMIN_USER_ID: z - .preprocess( - parseJsonSafe("BOT_ADMIN_USER_ID"), - z.array(z.coerce.number().safe()).or(z.coerce.number().safe()), - ) - .transform((v) => (Array.isArray(v) ? v : [v])) - .default([]), -}); +const createConfigFromEnvironment = (environment: NodeJS.ProcessEnv) => { + const config = parseEnv(environment, { + NODE_ENV: z.enum(["development", "production"]), + LOG_LEVEL: z + .enum(["trace", "debug", "info", "warn", "error", "fatal", "silent"]) + .default("info"), + BOT_MODE: { + schema: z.enum(["polling", "webhook"]), + defaults: { + production: "webhook" as const, + development: "polling" as const, + }, + }, + BOT_TOKEN: z.string(), + BOT_WEBHOOK: z.string().default(""), + BOT_SERVER_HOST: z.string().default("0.0.0.0"), + BOT_SERVER_PORT: port().default(80), + BOT_ALLOWED_UPDATES: z + .array(z.enum(API_CONSTANTS.ALL_UPDATE_TYPES)) + .default([]), + BOT_ADMINS: z.array(z.number()).default([]), + }); -const parseConfig = (environment: NodeJS.ProcessEnv) => { - const config = configSchema.parse(environment); + if (config.BOT_MODE === "webhook") { + // validate webhook url in webhook mode + z.string() + .url() + .parse(config.BOT_WEBHOOK, { + path: ["BOT_WEBHOOK"], + }); + } return { ...config, @@ -53,6 +42,6 @@ const parseConfig = (environment: NodeJS.ProcessEnv) => { }; }; -export type Config = ReturnType; +export type Config = ReturnType; -export const config = parseConfig(process.env); +export const config = createConfigFromEnvironment(process.env);