```
-
+
Start the bot in production mode:
```bash
npm start # with type checking (requires development dependencies)
@@ -117,7 +117,7 @@ project-root/
### Docker ([docker.com](https://docker.com))
Branch:
-[deploy/docker-compose](https://github.com/bot-base/telegram-bot-template/tree/deploy/docker-compose)
+[deploy/docker-compose](https://github.com/bot-base/telegram-bot-template/tree/deploy/docker-compose)
([open diff](https://github.com/bot-base/telegram-bot-template/compare/deploy/docker-compose))
Use in your project:
@@ -138,7 +138,7 @@ git merge template/deploy/docker-compose -X theirs --squash --no-commit --allow-
### Vercel ([vercel.com](https://vercel.com))
Branch:
-[deploy/vercel](https://github.com/bot-base/telegram-bot-template/tree/deploy/vercel)
+[deploy/vercel](https://github.com/bot-base/telegram-bot-template/tree/deploy/vercel)
([open diff](https://github.com/bot-base/telegram-bot-template/compare/deploy/vercel))
Use in your project:
@@ -161,7 +161,7 @@ git merge template/deploy/vercel -X theirs --squash --no-commit --allow-unrelate
### grammY conversations ([grammy.dev/plugins/conversations](https://grammy.dev/plugins/conversations))
Branch:
-[example/plugin-conversations](https://github.com/bot-base/telegram-bot-template/tree/example/plugin-conversations)
+[example/plugin-conversations](https://github.com/bot-base/telegram-bot-template/tree/example/plugin-conversations)
([open diff](https://github.com/bot-base/telegram-bot-template/compare/example/plugin-conversations))
Use in your project:
@@ -188,7 +188,7 @@ npm i @grammyjs/conversations
### grammY runner ([grammy.dev/plugins/runner](https://grammy.dev/plugins/runner))
Branch:
-[example/plugin-runner](https://github.com/bot-base/telegram-bot-template/tree/example/plugin-runner)
+[example/plugin-runner](https://github.com/bot-base/telegram-bot-template/tree/example/plugin-runner)
([open diff](https://github.com/bot-base/telegram-bot-template/compare/example/plugin-runner))
Use in your project:
@@ -215,7 +215,7 @@ npm i @grammyjs/runner
### Prisma ORM ([prisma.io](https://prisma.io))
Branch:
-[example/orm-prisma](https://github.com/bot-base/telegram-bot-template/tree/example/orm-prisma)
+[example/orm-prisma](https://github.com/bot-base/telegram-bot-template/tree/example/orm-prisma)
([open diff](https://github.com/bot-base/telegram-bot-template/compare/example/orm-prisma))
Use in your project:
@@ -243,7 +243,7 @@ npm i @prisma/client
### Bun ([bun.sh](https://bun.sh))
Branch:
-[example/runtime-bun](https://github.com/bot-base/telegram-bot-template/tree/example/runtime-bun)
+[example/runtime-bun](https://github.com/bot-base/telegram-bot-template/tree/example/runtime-bun)
([open diff](https://github.com/bot-base/telegram-bot-template/compare/example/runtime-bun))
Use in your project:
@@ -381,11 +381,11 @@ bun add -d @types/bun
Array of Number
- Optional.
- Administrator user IDs.
+ Optional.
+ Administrator user IDs.
Use this to specify user IDs that have special privileges, such as executing /setcommands .
Defaults to an empty array.
|
-
\ No newline at end of file
+
diff --git a/src/bot/callback-data/change-language.ts b/src/bot/callback-data/change-language.ts
index ca4bf18c..17ee5395 100644
--- a/src/bot/callback-data/change-language.ts
+++ b/src/bot/callback-data/change-language.ts
@@ -1,5 +1,5 @@
-import { createCallbackData } from "callback-data";
+import { createCallbackData } from 'callback-data'
-export const changeLanguageData = createCallbackData("language", {
+export const changeLanguageData = createCallbackData('language', {
code: String,
-});
+})
diff --git a/src/bot/callback-data/index.ts b/src/bot/callback-data/index.ts
index 8b4fda70..21c84d0b 100644
--- a/src/bot/callback-data/index.ts
+++ b/src/bot/callback-data/index.ts
@@ -1 +1 @@
-export * from "./change-language.js";
+export * from './change-language.js'
diff --git a/src/bot/context.ts b/src/bot/context.ts
index 09e3b008..ccff3acb 100644
--- a/src/bot/context.ts
+++ b/src/bot/context.ts
@@ -1,43 +1,43 @@
-import { Update, UserFromGetMe } from "@grammyjs/types";
-import { Context as DefaultContext, SessionFlavor, type Api } from "grammy";
-import type { AutoChatActionFlavor } from "@grammyjs/auto-chat-action";
-import type { HydrateFlavor } from "@grammyjs/hydrate";
-import type { I18nFlavor } from "@grammyjs/i18n";
-import type { ParseModeFlavor } from "@grammyjs/parse-mode";
-import type { Logger } from "#root/logger.js";
+import type { Update, UserFromGetMe } from '@grammyjs/types'
+import { type Api, Context as DefaultContext, type SessionFlavor } from 'grammy'
+import type { AutoChatActionFlavor } from '@grammyjs/auto-chat-action'
+import type { HydrateFlavor } from '@grammyjs/hydrate'
+import type { I18nFlavor } from '@grammyjs/i18n'
+import type { ParseModeFlavor } from '@grammyjs/parse-mode'
+import type { Logger } from '#root/logger.js'
-export type SessionData = {
+export interface SessionData {
// field?: string;
-};
+}
-type ExtendedContextFlavor = {
- logger: Logger;
-};
+interface ExtendedContextFlavor {
+ logger: Logger
+}
export type Context = ParseModeFlavor<
HydrateFlavor<
DefaultContext &
- ExtendedContextFlavor &
- SessionFlavor &
- I18nFlavor &
- AutoChatActionFlavor
+ ExtendedContextFlavor &
+ SessionFlavor &
+ I18nFlavor &
+ AutoChatActionFlavor
>
->;
+>
interface Dependencies {
- logger: Logger;
+ logger: Logger
}
export function createContextConstructor({ logger }: Dependencies) {
return class extends DefaultContext implements ExtendedContextFlavor {
- logger: Logger;
+ logger: Logger
constructor(update: Update, api: Api, me: UserFromGetMe) {
- super(update, api, me);
+ super(update, api, me)
this.logger = logger.child({
update_id: this.update.update_id,
- });
+ })
}
- } as unknown as new (update: Update, api: Api, me: UserFromGetMe) => Context;
+ } as unknown as new (update: Update, api: Api, me: UserFromGetMe) => Context
}
diff --git a/src/bot/features/admin.ts b/src/bot/features/admin.ts
index 142d6929..0bb84461 100644
--- a/src/bot/features/admin.ts
+++ b/src/bot/features/admin.ts
@@ -1,19 +1,19 @@
-import { chatAction } from "@grammyjs/auto-chat-action";
-import { Composer } from "grammy";
-import type { Context } from "#root/bot/context.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";
+import { chatAction } from '@grammyjs/auto-chat-action'
+import { Composer } from 'grammy'
+import type { Context } from '#root/bot/context.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 composer = new Composer()
-const feature = composer.chatType("private").filter(isAdmin);
+const feature = composer.chatType('private').filter(isAdmin)
feature.command(
- "setcommands",
- logHandle("command-setcommands"),
- chatAction("typing"),
+ 'setcommands',
+ logHandle('command-setcommands'),
+ chatAction('typing'),
setCommandsHandler,
-);
+)
-export { composer as adminFeature };
+export { composer as adminFeature }
diff --git a/src/bot/features/index.ts b/src/bot/features/index.ts
index 77f2609e..b838f8b4 100644
--- a/src/bot/features/index.ts
+++ b/src/bot/features/index.ts
@@ -1,4 +1,4 @@
-export * from "./admin.js";
-export * from "./language.js";
-export * from "./unhandled.js";
-export * from "./welcome.js";
+export * from './admin.js'
+export * from './language.js'
+export * from './unhandled.js'
+export * from './welcome.js'
diff --git a/src/bot/features/language.ts b/src/bot/features/language.ts
index b6fdbb3e..8626eae2 100644
--- a/src/bot/features/language.ts
+++ b/src/bot/features/language.ts
@@ -1,36 +1,36 @@
-import { Composer } from "grammy";
-import { changeLanguageData } from "#root/bot/callback-data/index.js";
-import type { Context } from "#root/bot/context.js";
-import { logHandle } from "#root/bot/helpers/logging.js";
-import { i18n } from "#root/bot/i18n.js";
-import { createChangeLanguageKeyboard } from "#root/bot/keyboards/index.js";
+import { Composer } from 'grammy'
+import { changeLanguageData } from '#root/bot/callback-data/index.js'
+import type { Context } from '#root/bot/context.js'
+import { logHandle } from '#root/bot/helpers/logging.js'
+import { i18n } from '#root/bot/i18n.js'
+import { createChangeLanguageKeyboard } from '#root/bot/keyboards/index.js'
-const composer = new Composer();
+const composer = new Composer()
-const feature = composer.chatType("private");
+const feature = composer.chatType('private')
-feature.command("language", logHandle("command-language"), async (ctx) => {
- return ctx.reply(ctx.t("language.select"), {
+feature.command('language', logHandle('command-language'), async (ctx) => {
+ return ctx.reply(ctx.t('language.select'), {
reply_markup: await createChangeLanguageKeyboard(ctx),
- });
-});
+ })
+})
feature.callbackQuery(
changeLanguageData.filter(),
- logHandle("keyboard-language-select"),
+ logHandle('keyboard-language-select'),
async (ctx) => {
const { code: languageCode } = changeLanguageData.unpack(
ctx.callbackQuery.data,
- );
+ )
if (i18n.locales.includes(languageCode)) {
- await ctx.i18n.setLocale(languageCode);
+ await ctx.i18n.setLocale(languageCode)
- return ctx.editMessageText(ctx.t("language.changed"), {
+ return ctx.editMessageText(ctx.t('language.changed'), {
reply_markup: await createChangeLanguageKeyboard(ctx),
- });
+ })
}
},
-);
+)
-export { composer as languageFeature };
+export { composer as languageFeature }
diff --git a/src/bot/features/unhandled.ts b/src/bot/features/unhandled.ts
index d4562ffb..af2b2e7d 100644
--- a/src/bot/features/unhandled.ts
+++ b/src/bot/features/unhandled.ts
@@ -1,17 +1,17 @@
-import { Composer } from "grammy";
-import type { Context } from "#root/bot/context.js";
-import { logHandle } from "#root/bot/helpers/logging.js";
+import { Composer } from 'grammy'
+import type { Context } from '#root/bot/context.js'
+import { logHandle } from '#root/bot/helpers/logging.js'
-const composer = new Composer();
+const composer = new Composer()
-const feature = composer.chatType("private");
+const feature = composer.chatType('private')
-feature.on("message", logHandle("unhandled-message"), (ctx) => {
- return ctx.reply(ctx.t("unhandled"));
-});
+feature.on('message', logHandle('unhandled-message'), (ctx) => {
+ return ctx.reply(ctx.t('unhandled'))
+})
-feature.on("callback_query", logHandle("unhandled-callback-query"), (ctx) => {
- return ctx.answerCallbackQuery();
-});
+feature.on('callback_query', logHandle('unhandled-callback-query'), (ctx) => {
+ return ctx.answerCallbackQuery()
+})
-export { composer as unhandledFeature };
+export { composer as unhandledFeature }
diff --git a/src/bot/features/welcome.ts b/src/bot/features/welcome.ts
index c1ef7e70..5f6b1072 100644
--- a/src/bot/features/welcome.ts
+++ b/src/bot/features/welcome.ts
@@ -1,13 +1,13 @@
-import { Composer } from "grammy";
-import type { Context } from "#root/bot/context.js";
-import { logHandle } from "#root/bot/helpers/logging.js";
+import { Composer } from 'grammy'
+import type { Context } from '#root/bot/context.js'
+import { logHandle } from '#root/bot/helpers/logging.js'
-const composer = new Composer();
+const composer = new Composer()
-const feature = composer.chatType("private");
+const feature = composer.chatType('private')
-feature.command("start", logHandle("command-start"), (ctx) => {
- return ctx.reply(ctx.t("welcome"));
-});
+feature.command('start', logHandle('command-start'), (ctx) => {
+ return ctx.reply(ctx.t('welcome'))
+})
-export { composer as welcomeFeature };
+export { composer as welcomeFeature }
diff --git a/src/bot/filters/index.ts b/src/bot/filters/index.ts
index f699fb48..43036632 100644
--- a/src/bot/filters/index.ts
+++ b/src/bot/filters/index.ts
@@ -1 +1 @@
-export * from "./is-admin.js";
+export * from './is-admin.js'
diff --git a/src/bot/filters/is-admin.ts b/src/bot/filters/is-admin.ts
index a2a3e1f2..969cb08e 100644
--- a/src/bot/filters/is-admin.ts
+++ b/src/bot/filters/is-admin.ts
@@ -1,4 +1,4 @@
-import { isUserHasId } from "grammy-guard";
-import { config } from "#root/config.js";
+import { isUserHasId } from 'grammy-guard'
+import { config } from '#root/config.js'
-export const isAdmin = isUserHasId(...config.BOT_ADMINS);
+export const isAdmin = isUserHasId(...config.BOT_ADMINS)
diff --git a/src/bot/handlers/commands/setcommands.ts b/src/bot/handlers/commands/setcommands.ts
index ee2be2e2..d43a6c61 100644
--- a/src/bot/handlers/commands/setcommands.ts
+++ b/src/bot/handlers/commands/setcommands.ts
@@ -1,41 +1,40 @@
-import { BotCommand } from "@grammyjs/types";
-import { CommandContext } from "grammy";
-import { i18n, isMultipleLocales } from "#root/bot/i18n.js";
-import { config } from "#root/config.js";
-import type { Context } from "#root/bot/context.js";
+import type { BotCommand } from '@grammyjs/types'
+import type { CommandContext } from 'grammy'
+import { i18n, isMultipleLocales } from '#root/bot/i18n.js'
+import { config } from '#root/config.js'
+import type { Context } from '#root/bot/context.js'
function getLanguageCommand(localeCode: string): BotCommand {
return {
- command: "language",
- description: i18n.t(localeCode, "language_command.description"),
- };
+ command: 'language',
+ description: i18n.t(localeCode, 'language_command.description'),
+ }
}
function getPrivateChatCommands(localeCode: string): BotCommand[] {
return [
{
- command: "start",
- description: i18n.t(localeCode, "start_command.description"),
+ command: 'start',
+ description: i18n.t(localeCode, 'start_command.description'),
},
- ];
+ ]
}
function getPrivateChatAdminCommands(localeCode: string): BotCommand[] {
return [
{
- command: "setcommands",
- description: i18n.t(localeCode, "setcommands_command.description"),
+ command: 'setcommands',
+ description: i18n.t(localeCode, 'setcommands_command.description'),
},
- ];
+ ]
}
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-function getGroupChatCommands(localeCode: string): BotCommand[] {
- return [];
+function getGroupChatCommands(_localeCode: string): BotCommand[] {
+ return []
}
export async function setCommandsHandler(ctx: CommandContext) {
- const DEFAULT_LANGUAGE_CODE = "en";
+ const DEFAULT_LANGUAGE_CODE = 'en'
// set private chat commands
await ctx.api.setMyCommands(
@@ -45,13 +44,13 @@ export async function setCommandsHandler(ctx: CommandContext) {
],
{
scope: {
- type: "all_private_chats",
+ type: 'all_private_chats',
},
},
- );
+ )
if (isMultipleLocales) {
- const requests = i18n.locales.map((code) =>
+ const requests = i18n.locales.map(code =>
ctx.api.setMyCommands(
[
...getPrivateChatCommands(code),
@@ -62,33 +61,33 @@ export async function setCommandsHandler(ctx: CommandContext) {
{
language_code: code,
scope: {
- type: "all_private_chats",
+ type: 'all_private_chats',
},
},
),
- );
+ )
- await Promise.all(requests);
+ await Promise.all(requests)
}
// set group chat commands
await ctx.api.setMyCommands(getGroupChatCommands(DEFAULT_LANGUAGE_CODE), {
scope: {
- type: "all_group_chats",
+ type: 'all_group_chats',
},
- });
+ })
if (isMultipleLocales) {
- const requests = i18n.locales.map((code) =>
+ const requests = i18n.locales.map(code =>
ctx.api.setMyCommands(getGroupChatCommands(code), {
language_code: code,
scope: {
- type: "all_group_chats",
+ type: 'all_group_chats',
},
}),
- );
+ )
- await Promise.all(requests);
+ await Promise.all(requests)
}
// set private chat commands for owner
@@ -100,11 +99,11 @@ export async function setCommandsHandler(ctx: CommandContext) {
],
{
scope: {
- type: "chat",
+ type: 'chat',
chat_id: Number(config.BOT_ADMINS),
},
},
- );
+ )
- return ctx.reply(ctx.t("admin.commands-updated"));
+ return ctx.reply(ctx.t('admin.commands-updated'))
}
diff --git a/src/bot/handlers/error.ts b/src/bot/handlers/error.ts
index 9d7c1ea2..936d962b 100644
--- a/src/bot/handlers/error.ts
+++ b/src/bot/handlers/error.ts
@@ -1,12 +1,12 @@
-import { ErrorHandler } from "grammy";
-import type { Context } from "#root/bot/context.js";
-import { getUpdateInfo } from "#root/bot/helpers/logging.js";
+import type { ErrorHandler } from 'grammy'
+import type { Context } from '#root/bot/context.js'
+import { getUpdateInfo } from '#root/bot/helpers/logging.js'
export const errorHandler: ErrorHandler = (error) => {
- const { ctx } = error;
+ const { ctx } = error
ctx.logger.error({
err: error.error,
update: getUpdateInfo(ctx),
- });
-};
+ })
+}
diff --git a/src/bot/handlers/index.ts b/src/bot/handlers/index.ts
index 3a6b76e3..46ed09fb 100644
--- a/src/bot/handlers/index.ts
+++ b/src/bot/handlers/index.ts
@@ -1,2 +1,2 @@
-export * from "./error.js";
-export * from "./commands/setcommands.js";
+export * from './error.js'
+export * from './commands/setcommands.js'
diff --git a/src/bot/helpers/keyboard.ts b/src/bot/helpers/keyboard.ts
index c3b1f53a..d97ee33e 100644
--- a/src/bot/helpers/keyboard.ts
+++ b/src/bot/helpers/keyboard.ts
@@ -1,7 +1,7 @@
export function chunk(array: T[], size: number) {
- const result = [];
- for (let index = 0; index < array.length; index += size) {
- result.push(array.slice(index, index + size));
- }
- return result;
+ const result = []
+ for (let index = 0; index < array.length; index += size)
+ result.push(array.slice(index, index + size))
+
+ return result
}
diff --git a/src/bot/helpers/logging.ts b/src/bot/helpers/logging.ts
index 401a2b89..58fb2f4f 100644
--- a/src/bot/helpers/logging.ts
+++ b/src/bot/helpers/logging.ts
@@ -1,21 +1,20 @@
-import { Middleware } from "grammy";
-import type { Update } from "@grammyjs/types";
-import type { Context } from "#root/bot/context.js";
+import type { Middleware } from 'grammy'
+import type { Update } from '@grammyjs/types'
+import type { Context } from '#root/bot/context.js'
-export function getUpdateInfo(ctx: Context): Omit {
- // eslint-disable-next-line camelcase, @typescript-eslint/no-unused-vars
- const { update_id, ...update } = ctx.update;
+export function getUpdateInfo(ctx: Context): Omit {
+ const { update_id, ...update } = ctx.update
- return update;
+ return update
}
export function logHandle(id: string): Middleware {
return (ctx, next) => {
ctx.logger.info({
msg: `Handle "${id}"`,
- ...(id.startsWith("unhandled") ? { update: getUpdateInfo(ctx) } : {}),
- });
+ ...(id.startsWith('unhandled') ? { update: getUpdateInfo(ctx) } : {}),
+ })
- return next();
- };
+ return next()
+ }
}
diff --git a/src/bot/i18n.ts b/src/bot/i18n.ts
index f909bcd5..ec90901e 100644
--- a/src/bot/i18n.ts
+++ b/src/bot/i18n.ts
@@ -1,14 +1,15 @@
-import path from "node:path";
-import { I18n } from "@grammyjs/i18n";
-import type { Context } from "#root/bot/context.js";
+import process from 'node:process'
+import path from 'node:path'
+import { I18n } from '@grammyjs/i18n'
+import type { Context } from '#root/bot/context.js'
export const i18n = new I18n({
- defaultLocale: "en",
- directory: path.resolve(process.cwd(), "locales"),
+ defaultLocale: 'en',
+ directory: path.resolve(process.cwd(), 'locales'),
useSession: true,
fluentBundleOptions: {
useIsolating: false,
},
-});
+})
-export const isMultipleLocales = i18n.locales.length > 1;
+export const isMultipleLocales = i18n.locales.length > 1
diff --git a/src/bot/index.ts b/src/bot/index.ts
index 653285e7..cdfe48ce 100644
--- a/src/bot/index.ts
+++ b/src/bot/index.ts
@@ -1,67 +1,68 @@
-import { autoChatAction } from "@grammyjs/auto-chat-action";
-import { hydrate } from "@grammyjs/hydrate";
-import { hydrateReply, parseMode } from "@grammyjs/parse-mode";
-import { BotConfig, StorageAdapter, Bot as TelegramBot, session } from "grammy";
-import {
+import { autoChatAction } from '@grammyjs/auto-chat-action'
+import { hydrate } from '@grammyjs/hydrate'
+import { hydrateReply, parseMode } from '@grammyjs/parse-mode'
+import type { BotConfig, StorageAdapter } from 'grammy'
+import { Bot as TelegramBot, session } from 'grammy'
+import type {
Context,
SessionData,
+} from '#root/bot/context.js'
+import {
createContextConstructor,
-} from "#root/bot/context.js";
+} from '#root/bot/context.js'
import {
adminFeature,
languageFeature,
unhandledFeature,
welcomeFeature,
-} from "#root/bot/features/index.js";
-import { errorHandler } from "#root/bot/handlers/index.js";
-import { i18n, isMultipleLocales } from "#root/bot/i18n.js";
-import { updateLogger } from "#root/bot/middlewares/index.js";
-import { config } from "#root/config.js";
-import { logger } from "#root/logger.js";
+} from '#root/bot/features/index.js'
+import { errorHandler } from '#root/bot/handlers/index.js'
+import { i18n, isMultipleLocales } from '#root/bot/i18n.js'
+import { updateLogger } from '#root/bot/middlewares/index.js'
+import { config } from '#root/config.js'
+import { logger } from '#root/logger.js'
-type Options = {
- sessionStorage?: StorageAdapter;
- config?: Omit, "ContextConstructor">;
-};
+interface Options {
+ sessionStorage?: StorageAdapter
+ config?: Omit, 'ContextConstructor'>
+}
export function createBot(token: string, options: Options = {}) {
- const { sessionStorage } = options;
+ const { sessionStorage } = options
const bot = new TelegramBot(token, {
...options.config,
ContextConstructor: createContextConstructor({ logger }),
- });
- const protectedBot = bot.errorBoundary(errorHandler);
+ })
+ const protectedBot = bot.errorBoundary(errorHandler)
// Middlewares
- bot.api.config.use(parseMode("HTML"));
+ bot.api.config.use(parseMode('HTML'))
- if (config.isDev) {
- protectedBot.use(updateLogger());
- }
+ if (config.isDev)
+ protectedBot.use(updateLogger())
- protectedBot.use(autoChatAction(bot.api));
- protectedBot.use(hydrateReply);
- protectedBot.use(hydrate());
+ protectedBot.use(autoChatAction(bot.api))
+ protectedBot.use(hydrateReply)
+ protectedBot.use(hydrate())
protectedBot.use(
session({
initial: () => ({}),
storage: sessionStorage,
}),
- );
- protectedBot.use(i18n);
+ )
+ protectedBot.use(i18n)
// Handlers
- protectedBot.use(welcomeFeature);
- protectedBot.use(adminFeature);
+ protectedBot.use(welcomeFeature)
+ protectedBot.use(adminFeature)
- if (isMultipleLocales) {
- protectedBot.use(languageFeature);
- }
+ if (isMultipleLocales)
+ protectedBot.use(languageFeature)
// must be the last handler
- protectedBot.use(unhandledFeature);
+ protectedBot.use(unhandledFeature)
- return bot;
+ return bot
}
-export type Bot = ReturnType;
+export type Bot = ReturnType
diff --git a/src/bot/keyboards/change-language.ts b/src/bot/keyboards/change-language.ts
index b6a5a2a0..5f0da8f8 100644
--- a/src/bot/keyboards/change-language.ts
+++ b/src/bot/keyboards/change-language.ts
@@ -1,22 +1,22 @@
-import { InlineKeyboard } from "grammy";
-import ISO6391 from "iso-639-1";
-import { changeLanguageData } from "#root/bot/callback-data/index.js";
-import type { Context } from "#root/bot/context.js";
-import { i18n } from "#root/bot/i18n.js";
-import { chunk } from "#root/bot/helpers/keyboard.js";
+import { InlineKeyboard } from 'grammy'
+import ISO6391 from 'iso-639-1'
+import { changeLanguageData } from '#root/bot/callback-data/index.js'
+import type { Context } from '#root/bot/context.js'
+import { i18n } from '#root/bot/i18n.js'
+import { chunk } from '#root/bot/helpers/keyboard.js'
-export const createChangeLanguageKeyboard = async (ctx: Context) => {
- const currentLocaleCode = await ctx.i18n.getLocale();
+export async function createChangeLanguageKeyboard(ctx: Context) {
+ const currentLocaleCode = await ctx.i18n.getLocale()
const getLabel = (code: string) => {
- const isActive = code === currentLocaleCode;
+ const isActive = code === currentLocaleCode
- return `${isActive ? "✅ " : ""}${ISO6391.getNativeName(code)}`;
- };
+ return `${isActive ? '✅ ' : ''}${ISO6391.getNativeName(code)}`
+ }
return InlineKeyboard.from(
chunk(
- i18n.locales.map((localeCode) => ({
+ i18n.locales.map(localeCode => ({
text: getLabel(localeCode),
callback_data: changeLanguageData.pack({
code: localeCode,
@@ -24,5 +24,5 @@ export const createChangeLanguageKeyboard = async (ctx: Context) => {
})),
2,
),
- );
-};
+ )
+}
diff --git a/src/bot/keyboards/index.ts b/src/bot/keyboards/index.ts
index 8b4fda70..21c84d0b 100644
--- a/src/bot/keyboards/index.ts
+++ b/src/bot/keyboards/index.ts
@@ -1 +1 @@
-export * from "./change-language.js";
+export * from './change-language.js'
diff --git a/src/bot/middlewares/index.ts b/src/bot/middlewares/index.ts
index 56e1e5ec..e8a6812f 100644
--- a/src/bot/middlewares/index.ts
+++ b/src/bot/middlewares/index.ts
@@ -1 +1 @@
-export * from "./update-logger.js";
+export * from './update-logger.js'
diff --git a/src/bot/middlewares/update-logger.ts b/src/bot/middlewares/update-logger.ts
index 96811203..082943c6 100644
--- a/src/bot/middlewares/update-logger.ts
+++ b/src/bot/middlewares/update-logger.ts
@@ -1,34 +1,35 @@
-import { performance } from "node:perf_hooks";
-import { Middleware } from "grammy";
-import type { Context } from "#root/bot/context.js";
-import { getUpdateInfo } from "#root/bot/helpers/logging.js";
+import { performance } from 'node:perf_hooks'
+import type { Middleware } from 'grammy'
+import type { Context } from '#root/bot/context.js'
+import { getUpdateInfo } from '#root/bot/helpers/logging.js'
export function updateLogger(): Middleware {
return async (ctx, next) => {
ctx.api.config.use((previous, method, payload, signal) => {
ctx.logger.debug({
- msg: "Bot API call",
+ msg: 'Bot API call',
method,
payload,
- });
+ })
- return previous(method, payload, signal);
- });
+ return previous(method, payload, signal)
+ })
ctx.logger.debug({
- msg: "Update received",
+ msg: 'Update received',
update: getUpdateInfo(ctx),
- });
+ })
- const startTime = performance.now();
+ const startTime = performance.now()
try {
- await next();
- } finally {
- const endTime = performance.now();
+ await next()
+ }
+ finally {
+ const endTime = performance.now()
ctx.logger.debug({
- msg: "Update processed",
+ msg: 'Update processed',
elapsed: endTime - startTime,
- });
+ })
}
- };
+ }
}
diff --git a/src/config.ts b/src/config.ts
index 3cd597f9..0ff44397 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -1,60 +1,61 @@
-import { loadEnvFile } from "node:process";
-import z from "zod";
-import { parseEnv, port } from "znv";
-import { API_CONSTANTS } from "grammy";
+import process from 'node:process'
+import z from 'zod'
+import { parseEnv, port } from 'znv'
+import { API_CONSTANTS } from 'grammy'
try {
- loadEnvFile();
-} catch {
+ process.loadEnvFile()
+}
+catch {
// No .env file found
}
-const createConfigFromEnvironment = (environment: NodeJS.ProcessEnv) => {
+function createConfigFromEnvironment(environment: NodeJS.ProcessEnv) {
const config = parseEnv(environment, {
- NODE_ENV: z.enum(["development", "production"]),
+ NODE_ENV: z.enum(['development', 'production']),
LOG_LEVEL: z
- .enum(["trace", "debug", "info", "warn", "error", "fatal", "silent"])
- .default("info"),
+ .enum(['trace', 'debug', 'info', 'warn', 'error', 'fatal', 'silent'])
+ .default('info'),
BOT_MODE: {
- schema: z.enum(["polling", "webhook"]),
+ schema: z.enum(['polling', 'webhook']),
defaults: {
- production: "webhook" as const,
- development: "polling" as const,
+ production: 'webhook' as const,
+ development: 'polling' as const,
},
},
BOT_TOKEN: z.string(),
- BOT_WEBHOOK: z.string().default(""),
- BOT_WEBHOOK_SECRET: z.string().default(""),
- BOT_SERVER_HOST: z.string().default("0.0.0.0"),
+ BOT_WEBHOOK: z.string().default(''),
+ BOT_WEBHOOK_SECRET: 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([]),
- });
+ })
- if (config.BOT_MODE === "webhook") {
+ if (config.BOT_MODE === 'webhook') {
// validate webhook url in webhook mode
z.string()
.url()
.parse(config.BOT_WEBHOOK, {
- path: ["BOT_WEBHOOK"],
- });
+ path: ['BOT_WEBHOOK'],
+ })
// validate webhook secret in webhook mode
z.string()
.min(1)
.parse(config.BOT_WEBHOOK_SECRET, {
- path: ["BOT_WEBHOOK_SECRET"],
- });
+ path: ['BOT_WEBHOOK_SECRET'],
+ })
}
return {
...config,
- isDev: process.env.NODE_ENV === "development",
- isProd: process.env.NODE_ENV === "production",
- };
-};
+ isDev: process.env.NODE_ENV === 'development',
+ isProd: process.env.NODE_ENV === 'production',
+ }
+}
-export type Config = ReturnType;
+export type Config = ReturnType
-export const config = createConfigFromEnvironment(process.env);
+export const config = createConfigFromEnvironment(process.env)
diff --git a/src/logger.ts b/src/logger.ts
index b69dda8c..811c92e5 100644
--- a/src/logger.ts
+++ b/src/logger.ts
@@ -1,5 +1,5 @@
-import { pino } from "pino";
-import { config } from "#root/config.js";
+import { pino } from 'pino'
+import { config } from '#root/config.js'
export const logger = pino({
level: config.LOG_LEVEL,
@@ -8,10 +8,10 @@ export const logger = pino({
...(config.isDev
? [
{
- target: "pino-pretty",
+ target: 'pino-pretty',
level: config.LOG_LEVEL,
options: {
- ignore: "pid,hostname",
+ ignore: 'pid,hostname',
colorize: true,
translateTime: true,
},
@@ -19,13 +19,13 @@ export const logger = pino({
]
: [
{
- target: "pino/file",
+ target: 'pino/file',
level: config.LOG_LEVEL,
options: {},
},
]),
],
},
-});
+})
-export type Logger = typeof logger;
+export type Logger = typeof logger
diff --git a/src/main.ts b/src/main.ts
index dfb4f2f8..939cc2ab 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,48 +1,50 @@
#!/usr/bin/env tsx
-import { serve } from "@hono/node-server";
-import { createBot } from "#root/bot/index.js";
-import { config } from "#root/config.js";
-import { logger } from "#root/logger.js";
-import { createServer } from "#root/server/index.js";
-import { AddressInfo } from "node:net";
+import process from 'node:process'
+import type { AddressInfo } from 'node:net'
+import { serve } from '@hono/node-server'
+import { createBot } from '#root/bot/index.js'
+import { config } from '#root/config.js'
+import { logger } from '#root/logger.js'
+import { createServer } from '#root/server/index.js'
function onShutdown(cleanUp: () => Promise) {
- let isShuttingDown = false;
+ let isShuttingDown = false
const handleShutdown = async () => {
- if (isShuttingDown) return;
- isShuttingDown = true;
- logger.info("Shutdown");
- await cleanUp();
- };
- process.on("SIGINT", handleShutdown);
- process.on("SIGTERM", handleShutdown);
+ if (isShuttingDown)
+ return
+ isShuttingDown = true
+ logger.info('Shutdown')
+ await cleanUp()
+ }
+ process.on('SIGINT', handleShutdown)
+ process.on('SIGTERM', handleShutdown)
}
async function startPolling() {
- const bot = createBot(config.BOT_TOKEN);
+ const bot = createBot(config.BOT_TOKEN)
// graceful shutdown
onShutdown(async () => {
- await bot.stop();
- });
+ await bot.stop()
+ })
// start bot
await bot.start({
allowed_updates: config.BOT_ALLOWED_UPDATES,
onStart: ({ username }) =>
logger.info({
- msg: "Bot running...",
+ msg: 'Bot running...',
username,
}),
- });
+ })
}
async function startWebhook() {
- const bot = createBot(config.BOT_TOKEN);
- const server = createServer(bot);
+ const bot = createBot(config.BOT_TOKEN)
+ const server = createServer(bot)
- let serverHandle: undefined | ReturnType;
+ let serverHandle: undefined | ReturnType
const startServer = () =>
new Promise((resolve) => {
serverHandle = serve(
@@ -51,54 +53,53 @@ async function startWebhook() {
hostname: config.BOT_SERVER_HOST,
port: config.BOT_SERVER_PORT,
},
- (info) => resolve(info),
- );
- });
+ info => resolve(info),
+ )
+ })
const stopServer = async () =>
new Promise((resolve) => {
- if (serverHandle) {
- serverHandle.close(() => resolve());
- } else {
- resolve();
- }
- });
+ if (serverHandle)
+ serverHandle.close(() => resolve())
+ else
+ resolve()
+ })
// graceful shutdown
onShutdown(async () => {
- await stopServer();
- });
+ await stopServer()
+ })
// to prevent receiving updates before the bot is ready
- await bot.init();
+ await bot.init()
// start server
- const info = await startServer();
+ const info = await startServer()
logger.info({
- msg: "Server started",
+ msg: 'Server started',
url:
- info.family === "IPv6"
+ info.family === 'IPv6'
? `http://[${info.address}]:${info.port}`
: `http://${info.address}:${info.port}`,
- });
+ })
// set webhook
await bot.api.setWebhook(config.BOT_WEBHOOK, {
allowed_updates: config.BOT_ALLOWED_UPDATES,
secret_token: config.BOT_WEBHOOK_SECRET,
- });
+ })
logger.info({
- msg: "Webhook was set",
+ msg: 'Webhook was set',
url: config.BOT_WEBHOOK,
- });
+ })
}
try {
- if (config.BOT_MODE === "webhook") {
- await startWebhook();
- } else if (config.BOT_MODE === "polling") {
- await startPolling();
- }
-} catch (error) {
- logger.error(error);
- process.exit(1);
+ if (config.BOT_MODE === 'webhook')
+ await startWebhook()
+ else if (config.BOT_MODE === 'polling')
+ await startPolling()
+}
+catch (error) {
+ logger.error(error)
+ process.exit(1)
}
diff --git a/src/server/index.ts b/src/server/index.ts
index 900645d2..7121bbed 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -1,39 +1,38 @@
-import { Hono } from "hono";
-import { HTTPException } from "hono/http-exception";
-import { webhookCallback } from "grammy";
-import type { Bot } from "#root/bot/index.js";
-import { config } from "#root/config.js";
-import { requestLogger } from "#root/server/middlewares/request-logger.js";
-import { getPath } from "hono/utils/url";
-import { Logger } from "#root/logger.js";
-import { requestId } from "./middlewares/request-id.js";
-import { logger } from "./middlewares/logger.js";
+import { Hono } from 'hono'
+import { HTTPException } from 'hono/http-exception'
+import { webhookCallback } from 'grammy'
+import { getPath } from 'hono/utils/url'
+import { requestId } from './middlewares/request-id.js'
+import { logger } from './middlewares/logger.js'
+import type { Bot } from '#root/bot/index.js'
+import { config } from '#root/config.js'
+import { requestLogger } from '#root/server/middlewares/request-logger.js'
+import type { Logger } from '#root/logger.js'
-type Variables = {
- requestId: string;
- logger: Logger;
-};
+interface Env {
+ Variables: {
+ requestId: string
+ logger: Logger
+ }
+}
-export const createServer = (bot: Bot) => {
- const server = new Hono<{
- Variables: Variables;
- }>();
+export function createServer(bot: Bot) {
+ const server = new Hono()
- server.use(requestId());
- server.use(logger());
+ server.use(requestId())
+ server.use(logger())
- if (config.isDev) {
- server.use(requestLogger());
- }
+ if (config.isDev)
+ server.use(requestLogger())
server.onError(async (error, c) => {
if (error instanceof HTTPException) {
- if (error.status < 500) {
- c.var.logger.info(error);
- } else {
- c.var.logger.error(error);
- }
- return error.getResponse();
+ if (error.status < 500)
+ c.var.logger.info(error)
+ else
+ c.var.logger.error(error)
+
+ return error.getResponse()
}
// unexpected error
@@ -41,25 +40,25 @@ export const createServer = (bot: Bot) => {
err: error,
method: c.req.raw.method,
path: getPath(c.req.raw),
- });
+ })
return c.json(
{
- error: "Oops! Something went wrong.",
+ error: 'Oops! Something went wrong.',
},
500,
- );
- });
+ )
+ })
- server.get("/", (c) => c.json({ status: true }));
+ server.get('/', c => c.json({ status: true }))
server.post(
- "/webhook",
- webhookCallback(bot, "hono", {
+ '/webhook',
+ webhookCallback(bot, 'hono', {
secretToken: config.BOT_WEBHOOK_SECRET,
}),
- );
+ )
- return server;
-};
+ return server
+}
-export type Server = Awaited>;
+export type Server = Awaited>
diff --git a/src/server/middlewares/logger.ts b/src/server/middlewares/logger.ts
index 255cefff..fb553c3b 100644
--- a/src/server/middlewares/logger.ts
+++ b/src/server/middlewares/logger.ts
@@ -1,15 +1,15 @@
-import { logger as _logger } from "#root/logger.js";
-import { MiddlewareHandler } from "hono";
+import type { MiddlewareHandler } from 'hono'
+import { logger as _logger } from '#root/logger.js'
export function logger(): MiddlewareHandler {
return async (c, next) => {
c.set(
- "logger",
+ 'logger',
_logger.child({
- requestId: c.get("requestId"),
+ requestId: c.get('requestId'),
}),
- );
+ )
- await next();
- };
+ await next()
+ }
}
diff --git a/src/server/middlewares/request-id.ts b/src/server/middlewares/request-id.ts
index b375484b..5ddeadeb 100644
--- a/src/server/middlewares/request-id.ts
+++ b/src/server/middlewares/request-id.ts
@@ -1,10 +1,10 @@
-import { MiddlewareHandler } from "hono";
-import { randomUUID } from "node:crypto";
+import { randomUUID } from 'node:crypto'
+import type { MiddlewareHandler } from 'hono'
export function requestId(): MiddlewareHandler {
return async (c, next) => {
- c.set("requestId", randomUUID());
+ c.set('requestId', randomUUID())
- await next();
- };
+ await next()
+ }
}
diff --git a/src/server/middlewares/request-logger.ts b/src/server/middlewares/request-logger.ts
index 213546a9..efbdc5a1 100644
--- a/src/server/middlewares/request-logger.ts
+++ b/src/server/middlewares/request-logger.ts
@@ -1,27 +1,27 @@
-import { MiddlewareHandler } from "hono";
-import { getPath } from "hono/utils/url";
+import type { MiddlewareHandler } from 'hono'
+import { getPath } from 'hono/utils/url'
export function requestLogger(): MiddlewareHandler {
return async (c, next) => {
- const { method } = c.req;
- const path = getPath(c.req.raw);
+ const { method } = c.req
+ const path = getPath(c.req.raw)
c.var.logger.debug({
- msg: "Incoming request",
+ msg: 'Incoming request',
method,
path,
- });
- const startTime = performance.now();
+ })
+ const startTime = performance.now()
- await next();
+ await next()
- const endTime = performance.now();
+ const endTime = performance.now()
c.var.logger.debug({
- msg: "Request completed",
+ msg: 'Request completed',
method,
path,
status: c.res.status,
elapsed: endTime - startTime,
- });
- };
+ })
+ }
}
diff --git a/tsconfig.json b/tsconfig.json
index 6991dda9..fca6c24c 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,21 +1,21 @@
{
"compilerOptions": {
- "strict": true,
- "skipLibCheck": true,
- "esModuleInterop": true,
- "preserveWatchOutput": true,
- "noEmit": true,
- "module": "NodeNext",
"target": "ES2021",
- "moduleResolution": "NodeNext",
- "sourceMap": true,
- "outDir": "build",
"rootDir": ".",
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
"paths": {
"#root/*": [
"./src/*"
]
- }
+ },
+ "strict": true,
+ "noEmit": true,
+ "outDir": "build",
+ "sourceMap": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "preserveWatchOutput": true
},
"include": [
"src/**/*"