Одна из его самых сильных сторон Telegram — это интеграция ботов прямо в экосистему приложения. В отличие от многих других платформ, он предоставляет разработчикам мощный API, позволяющий создавать разнообразных ботов — от простых информационных помощников до сложных интерактивных сервисов. Боты в Telegram — это программные интерфейсы, которые выполняют задачи внутри мессенджера и выглядят как обычные пользователи, только с пометкой "бот" в имени. Они могут отправлять сообщения, присоединяться к группам и каналам, обрабатывать платежи и выполнять множество других операций. Уникальность Telegram-ботов в том, что их функциональность ограничена только фантазией разработчика.
Для создания таких ботов существует несколько инструментов, но grammY выделяется как современная и удобная библиотека, специально разработанная для TypeScript. Она предлагает простой, но гибкий API, который тесно связан с официальным Telegram Bot API, поэтому разработчики не теряют никаких возможностей. Примечательно, что grammY работает на разных JavaScript-платформах, включая Node.js и Deno, что делает её универсальным инструментом для различных сред разработки. Библиотека предоставляет полный доступ к функциям Telegram Bot API через удобный интерфейс, заточенный под TypeScript.
Почему именно TypeScript? Строгая типизация делает код более безопасным и предсказуемым, что критично при разработке ботов, которые должны обрабатывать множество различных событий и пользовательских действий. TypeScript помогает избежать ошибок на этапе компиляции, а не во время работы бота, что существенно упрощает отладку и поддержку кода.
В этой статье мы шаг за шагом разберем процесс создания Telegram бота с использованием TypeScript и grammY. Мы начнем с базовой настройки проекта, а затем перейдем к реализации различных функций, включая обработку текстовых сообщений, голосовых заметок и изображений. Также рассмотрим, как интегрировать бота с внешними API, например, с Google Gemini для генерации ответов на основе искусственного интеллекта.
Обзор технологий grammY для создания Telegram-ботов
GrammY представляет собой современный фреймворк, специально разработанный для создания Telegram-ботов с использованием TypeScript. Название фреймворка — это игра слов, объединяющая "grammar" (грамматика) и букву "Y", что отражает структурированный подход к построению ботов и связь с экосистемой TypeScript.
Ядро grammY построено как тонкая обертка над официальным Telegram Bot API, но при этом предоставляет множество удобных инструментов, которые значительно упрощают процесс разработки. Фреймворк автоматически преобразует необработанные HTTP-запросы и ответы в удобные объекты с типизацией, позволяя разработчикам сосредоточиться на бизнес-логике, а не деталях взаимодействия с API. Одной из ключевых концепций grammY является система обработчиков событий. Она позволяет легко регистрировать функции, которые будут выполняться при получении определенных типов сообщений или команд:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
| // Обработка текстовых сообщений
bot.on('message:text', async (ctx) => {
const prompt: string = ctx.message.text;
// Обработка сообщения
return ctx.reply('Ответ на ваше сообщение');
});
// Обработка конкретных команд
bot.command('start', async (ctx) => {
return ctx.reply('Добро пожаловать!');
}); |
|
Фреймворк предоставляет специализированные методы для различных типов взаимодействия, включая:
bot.command() — для обработки команд (например, /start),
bot.on() — универсальный метод для обработки различных событий,
bot.hears() — для отслеживания конкретных текстовых паттернов,
bot.action() — для работы с действиями от инлайн-кнопок.
Для обработки медиафайлов grammY предлагает интуитивно понятный интерфейс. Например, для работы с изображениями или голосовыми сообщениями используются соответствующие обработчики:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
| // Обработка фотографий
bot.on('message:photo', async (ctx) => {
const photoFile = await ctx.getFile();
// Дальнейшая обработка фото
});
// Обработка голосовых сообщений
bot.on('message:voice', async (ctx) => {
const file = await ctx.getFile();
// Работа с голосовым сообщением
}); |
|
Контекст взаимодействия (ctx) — другая важная концепция grammY. Это объект, который содержит всю информацию о текущем взаимодействии с пользователем: само сообщение, данные отправителя, а также методы для ответа. Благодаря TypeScript, разработчик получает автоподсказки и проверку типов при работе с этим объектом, что снижает вероятность ошибок. GrammY также включает встроенные механизмы обработки ошибок. При возникновении проблемы во время выполнения обработчика, фреймворк может автоматически обработать её через метод bot.catch():
| TypeScript | 1
2
3
4
5
| bot.catch((error) => {
const ctx = error.ctx;
console.log(error);
return ctx.reply('Произошла ошибка. Попробуйте позже!');
}); |
|
Еще одним мощным инструментом grammY является система middleware, позволяющая создавать модульные и повторно используемые компоненты для обработки сообщений. Middleware — это функции, которые выполняются последовательно и могут модифицировать контекст или прерывать цепочку обработки:
| TypeScript | 1
2
3
4
5
| // Простой middleware для логирования
bot.use(async (ctx, next) => {
console.log(`Получено сообщение от ${ctx.from?.first_name}`);
await next(); // Передача управления следующему обработчику
}); |
|
При работе с сессиями и состояниями пользователей grammY предлагает гибкую систему сессий, которая может быть интегрирована с различными хранилищами данных — от простых объектов в памяти до баз данных. Для масштабных проектов grammY предоставляет "long polling" и webhook режимы работы, а также инструменты для управления большими объемами запросов.
Фреймворк активно развивается, и его экосистема постоянно пополняется плагинами для решения специфических задач: от интеграции с базами данных до создания сложных форм и сценариев взаимодействия. Сильной стороной grammY является универсальность — фреймворк работает не только в Node.js, но и в Deno, а также поддерживает среды выполнения, совместимые с веб-стандартами, что делает его подходящим для развертывания в различных окружениях, включая serverless-платформы.
TypeScript vs Script# vs У кого какой опыт ? - сравнительные достоинства и недостатки. Перевод C# на TypeScript Доброго времени суток))) (Извините если не в ту тему)
Существует рабочая программы для локального... VS2012 + typescript 9.1.1 При работе с TypeScript VS2012 виснет или закрывается регулярно, никакой конкретной информации об... Создать редактор радиосхем для MVC5, используя TypeScript Ребята!)
Нужна инфа как возможно создать редактор,используя typescript (js нежелательно),на mvc 5...
Преимущества TypeScript в разработке ботов
Разработка ботов требует особого внимания к обработке различных типов данных и событий. В этом контексте TypeScript выступает мощным союзником предоставляя ряд значительных преимуществ по сравнению с чистым JavaScript. Статическая типизация — главное преимущество TypeScript при создании ботов. Telegram Bot API работает с множеством различных объектов: сообщения, пользователи, чаты, медиафайлы и т.д. Каждый из них имеет свою структуру и набор свойств. TypeScript позволяет создавать типы и интерфейсы, четко отражающие эти структуры:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| interface User {
id: number;
is_bot: boolean;
first_name: string;
last_name?: string;
username?: string;
}
interface Message {
message_id: number;
from?: User;
date: number;
chat: Chat;
text?: string;
photo?: PhotoSize[];
// другие поля...
} |
|
Такой подход приносит несколько ключевых выгод. Во-первых, типизация позволяет обнаруживать ошибки на этапе компиляции, а не во время работы бота. Когда разработчик пытается использовать несуществующее свойство объекта или передать неверный тип данных, компилятор TypeScript немедленно сигнализирует об этом:
| TypeScript | 1
2
3
4
5
| // Ошибка будет обнаружена при компиляции, а не в рантайме
bot.on('message:text', (ctx) => {
const userName = ctx.message.fromUser.username; // Ошибка! Правильно: ctx.message.from?.username
// ...
}); |
|
Во-вторых, современные редакторы кода, такие как VS Code, используют информацию о типах для предоставления интеллектуальных подсказок и автодополнения. При работе с контекстом сообщения (ctx) разработчик видит все доступные методы и свойства, что значительно ускоряет написание кода и снижает вероятность ошибок. Утилиты типов TypeScript особенно полезны при моделировании сложных взаимодействий. Например, при создании бота с многоступенчатым диалогом можно описать каждое состояние диалога и возможные переходы между ними:
| TypeScript | 1
2
3
4
5
6
7
8
9
| type DialogState = 'initial' | 'awaitingName' | 'awaitingEmail' | 'confirmation';
interface SessionData {
state: DialogState;
userData: {
name?: string;
email?: string;
};
} |
|
Дженерики в TypeScript позволяют создавать универсальные компоненты с типобезопасностью. Например, при работе с различными типами сообщений:
| TypeScript | 1
2
3
| function processMessage<T extends Message>(message: T, processor: (msg: T) => string): string {
return processor(message);
} |
|
При работе с асинхронными операциями, которые часто встречаются в ботах (запросы к API, операции с базами данных), TypeScript обеспечивает типобезопасность для Promise-объектов и конструкций async/await:
| TypeScript | 1
2
3
4
| async function handleUserQuery(userId: number): Promise<string> {
const userData = await database.getUserData(userId);
return formatResponse(userData);
} |
|
В командной разработке TypeScript выступает как своего рода документация, делая код более понятным для новых участников проекта. Интерфейсы четко определяют структуру данных, с которыми работает бот, что упрощает ориентацию в кодовой базе.
При рефакторинге и масштабировании бота типизация позволяет уверенно вносить изменения, так как компилятор укажет на все места, которые требуют корректировки. Это особенно ценно, когда функциональность бота расширяется и усложняется его структура.
Наконец, экосистема TypeScript включает множество утилит и библиотек, специфичных для разработки Telegram-ботов, включая типизированные обертки над Bot API, такие как grammY. Эти инструменты, сочетаясь с TypeScript, создают мощную основу для разработки надежных и функциональных ботов.
Сравнение grammY с другими библиотеками для разработки Telegram-ботов
На рынке существует несколько библиотек для создания Telegram-ботов на JavaScript/TypeScript, и каждая имеет свои особенности. Чтобы лучше понять позицию grammY в этой экосистеме, стоит сравнить её с основными конкурентами.
Telegraf долгое время был стандартом для разработки ботов на Node.js. Он предлагает богатый API и обширные возможности, но grammY выделяется улучшенной поддержкой TypeScript из коробки. В то время как Telegraf добавил поддержку TypeScript постфактум, grammY изначально проектировался с учетом статической типизации. Это особенно заметно при работе с контекстом сообщений — в grammY типы более точны и требуют меньше ручных аннотаций:
| TypeScript | 1
2
3
4
5
6
7
| // grammY
bot.on('message:text', (ctx) => {
// TypeScript автоматически определяет тип ctx.message как Message.TextMessage
const text = ctx.message.text; // Безопасно, без необходимости проверок
});
// Telegraf требует больше явных проверок типов в некоторых сценариях |
|
node-telegram-bot-api — низкоуровневая библиотека, которая предоставляет прямой доступ к Telegram Bot API. В отличие от неё, grammY предлагает более высокоуровневые абстракции, сохраняя при этом доступ ко всем возможностям API. Вот как выглядит обработка команды в обеих библиотеках:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
| // node-telegram-bot-api
bot.onText(/\/start/, (msg) => {
const chatId = msg.chat.id;
bot.sendMessage(chatId, 'Привет!');
});
// grammY
bot.command('start', (ctx) => {
return ctx.reply('Привет!');
}); |
|
Код grammY не только короче, но и более выразителен. Методы встроены в контекст сообщения, что делает код более читаемым и поддерживаемым.
Botgram и Telebot — альтернативные библиотеки, которые также упрощают разработку ботов, но они менее активно развиваются и имеют ограниченную поддержку современных функций Telegram Bot API. GrammY регулярно обновляется и быстро поддерживает новые возможности Telegram.
Важное преимущество grammY — его совместимость с различными JavaScript-средами. В то время как большинство библиотек работают только с Node.js, grammY поддерживает также Deno и среды, совместимые с веб-стандартами. Это делает библиотеку универсальным инструментом, подходящим для различных сценариев развертывания. В плане производительности grammY оптимизирован для обработки большого количества сообщений. Библиотека предлагает функции для управления высокой нагрузкой, такие как автоматическое управление соединениями и очередями сообщений.
Система плагинов — еще одна сильная сторона grammY. Благодаря модульной архитектуре, разработчики могут легко расширять функциональность библиотеки, не перегружая основной код:
| TypeScript | 1
2
3
| // Подключение плагина для работы с сессиями
import { conversations } from '@grammyjs/conversations';
bot.use(conversations()); |
|
Документация grammY тщательно проработана и включает множество примеров, что существенно сокращает время на освоение библиотеки. Хотя сообщество grammY меньше, чем у некоторых более старых библиотек, оно активно растет благодаря современному подходу и хорошей поддержке. В контексте TypeScript-проектов grammY предлагает наиболее целостный опыт разработки среди всех конкурентов. Типы определены точно и полно, что минимизирует необходимость в дополнительных аннотациях и обходных решениях. Это особенно важно при работе с комплексными взаимодействиями и интеграциями сторонних сервисов.
Для новых проектов на TypeScript grammY представляет оптимальный баланс между простотой использования, современностью подхода и гибкостью, делая его привлекательным выбором среди доступных инструментов для разработки Telegram-ботов.
Архитектурные паттерны при разработке Telegram-ботов
При разработке Telegram-ботов выбор правильной архитектуры играет критическую роль в создании масштабируемых и поддерживаемых решений. Множество традиционных паттернов проектирования могут быть адаптированы для ботов, учитывая их событийно-ориентированную природу.
Одним из фундаментальных подходов является адаптация паттерна MVC (Model-View-Controller) для ботов. В этом контексте:
Model представляет данные и бизнес-логику (пользовательские профили, состояния диалогов).
View — это форматирование ответов пользователю (текст, кнопки, карточки).
Controller — обработчики событий, которые принимают входящие сообщения.
В grammY такая структура может быть реализована через разделение кода на соответствующие модули:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| // model.ts - работа с данными
export async function getUserData(userId: number): Promise<UserData> {
return await database.findUser(userId);
}
// view.ts - форматирование ответов
export function formatWelcomeMessage(user: UserData): string {
return `Здравствуйте, ${user.name}! Чем могу помочь?`;
}
// controller.ts - обработчики команд
bot.command('start', async (ctx) => {
const userId = ctx.from?.id;
if (!userId) return;
const userData = await getUserData(userId);
const message = formatWelcomeMessage(userData);
return ctx.reply(message);
}); |
|
Паттерн "Состояние" (State) особенно полезен для ботов с многоступенчатыми диалогами. Он позволяет изменять поведение бота в зависимости от текущего состояния разговора с пользователем:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| enum DialogState {
INITIAL,
AWAITING_NAME,
AWAITING_EMAIL,
COMPLETED
}
interface UserSession {
state: DialogState;
data: Record<string, any>;
}
// Обработка сообщений в зависимости от состояния
bot.on('message:text', async (ctx) => {
const userId = ctx.from?.id;
if (!userId) return;
const session = await getUserSession(userId);
switch (session.state) {
case DialogState.AWAITING_NAME:
session.data.name = ctx.message.text;
session.state = DialogState.AWAITING_EMAIL;
await saveUserSession(userId, session);
return ctx.reply('Отлично! Теперь введите ваш email:');
case DialogState.AWAITING_EMAIL:
// Обработка email
// ...
default:
// Обработка по умолчанию
}
}); |
|
Middleware в grammY реализует паттерн "Цепочка обязанностей" (Chain of Responsibility), где каждый middleware может обрабатывать часть запроса и передавать управление следующему в цепочке:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| // Аутентификация пользователя
bot.use(async (ctx, next) => {
const userId = ctx.from?.id;
if (!userId) {
return ctx.reply('Ошибка аутентификации');
}
ctx.state.user = await authenticateUser(userId);
await next(); // Передача управления следующему middleware
});
// Логирование действий
bot.use(async (ctx, next) => {
const startTime = Date.now();
await next();
const ms = Date.now() - startTime;
console.log(`Обработка запроса заняла ${ms}ms`);
}); |
|
Паттерн "Фабрика" (Factory) может использоваться для создания различных типов ответов бота:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| class MessageFactory {
static createTextResponse(text: string) {
return { text, parse_mode: 'Markdown' };
}
static createPhotoResponse(photoUrl: string, caption?: string) {
return { photo: photoUrl, caption };
}
static createKeyboardResponse(text: string, buttons: any[]) {
return {
text,
reply_markup: { inline_keyboard: buttons }
};
}
}
// Использование фабрики
bot.command('menu', (ctx) => {
const response = MessageFactory.createKeyboardResponse(
'Выберите опцию:',
[[{ text: 'Профиль', callback_data: 'profile' }]]
);
return ctx.reply(response.text, { reply_markup: response.reply_markup });
}); |
|
Паттерн "Команда" (Command) позволяет инкапсулировать запрос как объект, что удобно для организации множества команд бота:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| interface BotCommand {
execute(ctx: Context): Promise<void>;
}
class StartCommand implements BotCommand {
async execute(ctx: Context): Promise<void> {
await ctx.reply('Добро пожаловать!');
}
}
class HelpCommand implements BotCommand {
async execute(ctx: Context): Promise<void> {
await ctx.reply('Список доступных команд: /start, /help, /settings');
}
}
// Регистрация команд
const commands: Record<string, BotCommand> = {
'start': new StartCommand(),
'help': new HelpCommand(),
// другие команды...
};
// Обработка команд
bot.on('message:text', async (ctx) => {
const text = ctx.message.text;
if (text.startsWith('/')) {
const commandName = text.slice(1).split(' ')[0];
const command = commands[commandName];
if (command) {
await command.execute(ctx);
return;
}
}
// Обработка обычных сообщений
}); |
|
Для модульной организации кода бота эффективна "Модульная архитектура", когда бот разделен на функциональные модули, каждый из которых отвечает за определённый набор возможностей:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // profiles/index.ts
export function setupProfileModule(bot: Bot) {
bot.command('profile', profileCommandHandler);
bot.action(/^profile:/, profileActionHandler);
// Другие обработчики связанные с профилем
}
// orders/index.ts
export function setupOrdersModule(bot: Bot) {
bot.command('order', orderCommandHandler);
bot.on('message:location', locationHandler);
// Другие обработчики связанные с заказами
}
// Подключение модулей
setupProfileModule(bot);
setupOrdersModule(bot); |
|
Использование архитектурных паттернов при разработке Telegram-ботов помогает создавать структурированный, расширяемый и легко поддерживаемый код. GrammY, благодаря своей гибкости и хорошей интеграции с TypeScript, делает применение этих паттернов особенно удобным.
Экосистема grammY: плагины и расширения
Одной из сильнейших сторон grammY является его расширяемая архитектура, построенная на принципе модульности. Разработчики фреймворка осознают, что невозможно предусмотреть все сценарии использования в основной библиотеке, поэтому они создали надежную экосистему плагинов и расширений, которые можно подключать по мере необходимости. Официальные плагины grammY можно разделить на несколько категорий в зависимости от их функциональности. Среди наиболее востребованных — плагин для работы с сессиями @grammyjs/storage-*, который позволяет сохранять состояние диалога с пользователем:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| import { session } from "grammy";
import { FileAdapter } from "@grammyjs/storage-file";
// Определение типа данных сессии
interface SessionData {
counter: number;
lastVisited: Date;
}
// Инициализация хранилища
const adapter = new FileAdapter<SessionData>({
dirName: "sessions"
});
// Подключение middleware для сессий
bot.use(session({
initial: () => ({ counter: 0, lastVisited: new Date() }),
storage: adapter
}));
bot.command("stats", async (ctx) => {
// Увеличение счетчика в сессии
ctx.session.counter++;
await ctx.reply(`Вы использовали эту команду ${ctx.session.counter} раз`);
}); |
|
Другой важный компонент — плагин @grammyjs/conversations, который упрощает создание многошаговых диалогов с пользователем:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| import { conversations } from "@grammyjs/conversations";
bot.use(conversations());
// Определение диалога регистрации
const registerConversation = async (conversation, ctx) => {
await ctx.reply("Как вас зовут?");
const { message } = await conversation.wait();
const name = message.text;
await ctx.reply("Укажите ваш email:");
const { message: emailMessage } = await conversation.wait();
const email = emailMessage.text;
// Сохранение данных в БД
await saveUser({ name, email });
await ctx.reply(`Спасибо, ${name}! Регистрация завершена.`);
};
bot.command("register", async (ctx) => {
await ctx.conversation.enter("registerConversation");
}); |
|
Для работы с интерактивными меню предназначен плагин @grammyjs/menu, который значительно упрощает построение сложных интерфейсов с кнопками:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| import { Menu } from "@grammyjs/menu";
// Создание меню с несколькими кнопками
const menu = new Menu("main-menu")
.text("Профиль", (ctx) => ctx.reply("Ваш профиль"))
.text("Настройки", (ctx) => ctx.reply("Ваши настройки"))
.row()
.url("Помощь", "https://example.com/help");
// Регистрация меню в боте
bot.use(menu);
bot.command("menu", async (ctx) => {
await ctx.reply("Выберите опцию:", { reply_markup: menu });
}); |
|
Для ботов, требующих локализации на несколько языков, существует плагин @grammyjs/i18n, который предоставляет удобный механизм перевода:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
| import { I18n } from "@grammyjs/i18n";
const i18n = new I18n({
defaultLocale: "ru",
directory: "locales",
// locales/ru.json, locales/en.json и т.д.
});
bot.use(i18n.middleware());
bot.command("hello", (ctx) => {
return ctx.reply(ctx.t("greeting", { name: ctx.from.first_name }));
}); |
|
Для грамотной обработки скоростных ограничений API Telegram разработчики могут воспользоваться плагином @grammyjs/ratelimiter, который автоматически управляет частотой запросов:
| TypeScript | 1
2
3
4
5
6
7
| import { limit } from "@grammyjs/ratelimiter";
bot.use(limit({
timeFrame: 1000, // 1 секунда
limit: 3, // максимум 3 сообщения в секунду от одного пользователя
onLimitExceeded: (ctx) => ctx.reply("Пожалуйста, не отправляйте сообщения так часто!")
})); |
|
Для разработки ботов, которым требуется обрабатывать большое количество сообщений, существует плагин @grammyjs/runner, предлагающий различные стратегии обработки обновлений:
| TypeScript | 1
2
3
4
5
6
7
| import { run, sequentialize } from "@grammyjs/runner";
// Последовательная обработка сообщений от одного пользователя
bot.use(sequentialize((ctx) => ctx.from?.id.toString()));
// Запуск бота с оптимизированным механизмом обработки обновлений
run(bot); |
|
Кроме официальных, существует растущее число сторонних плагинов, разработанных сообществом. Они охватывают самые разные функции — от аналитики до интеграции с различными API.
Модульная архитектура grammY также позволяет разработчикам легко создавать собственные плагины. Типичный плагин представляет собой middleware-функцию которая может быть подключена через метод bot.use():
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // Пример создания простого логирующего плагина
function createLoggerPlugin(options: { level: 'debug' | 'info' | 'error' }) {
return async (ctx, next) => {
const start = Date.now();
console.log(`${options.level}: Получено сообщение от ${ctx.from?.first_name || 'неизвестного пользователя'}`);
await next();
const ms = Date.now() - start;
console.log(`${options.level}: Ответ отправлен за ${ms}ms`);
};
}
// Использование
bot.use(createLoggerPlugin({ level: 'info' })); |
|
Эта экосистема плагинов превращает grammY из просто библиотеки в полноценную платформу для разработки Telegram-ботов любой сложности, позволяя разработчикам собирать своих ботов из готовых, хорошо протестированных модулей и сосредоточиться на уникальной бизнес-логике своего приложения.
Настройка окружения
Перед тем как погрузиться в разработку Telegram-бота, необходимо правильно настроить рабочее окружение. Это фундамент, на котором будет строиться весь проект, и от его качества зависит удобство и эффективность дальнейшей работы.
Начнем с создания нового проекта. Для этого создайте директорию для вашего бота и инициализируйте в ней npm-проект:
| Bash | 1
2
3
| mkdir telegram-bot
cd telegram-bot
npm init -y |
|
Команда npm init -y создаст базовый файл package.json с настройками по умолчанию. Теперь необходимо установить основные зависимости для работы с grammY и TypeScript:
| Bash | 1
2
| npm install grammy
npm install --save-dev typescript @types/node |
|
Здесь grammy — основная библиотека для разработки бота, а typescript и @types/node нужны для поддержки TypeScript в Node.js среде.
Следующий шаг — инициализация TypeScript в проекте:
Эта команда создаст файл tsconfig.json, который определяет, как компилятор TypeScript будет обрабатывать код. Для работы с современным синтаксисом JavaScript и модулями ES стоит отредактировать его следующим образом:
| JSON | 1
2
3
4
5
6
7
8
9
10
| {
"compilerOptions": {
"target": "es2017",
"module": "nodenext",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
} |
|
Теперь создадим базовую структуру файлов проекта:
| TypeScript | 1
2
3
4
5
6
7
| telegram-bot/
├── .env # Файл для переменных окружения
├── .gitignore # Список игнорируемых Git файлов
├── src/ # Директория с исходным кодом
│ └── bot.ts # Основной файл бота
├── package.json # Настройки npm
└── tsconfig.json # Настройки TypeScript |
|
В файл .gitignore следует добавить следующие строки, чтобы случайно не выложить в репозиторий конфиденциальные данные или сгенерированные файлы:
| TypeScript | 1
2
3
4
| node_modules/
.env
dist/
*.js |
|
Для управления переменными окружения, такими как токен бота, удобно использовать пакет dotenv. Установите его:
И создайте файл .env в корне проекта:
| TypeScript | 1
| TELEGRAM_BOT_TOKEN=ваш_токен_от_botfather |
|
Чтобы облегчить процесс разработки, настройте скрипты в package.json:
| JSON | 1
2
3
4
5
6
7
| {
"scripts": {
"build": "tsc",
"start": "node --env-file=.env dist/bot.js",
"dev": "tsc -w & node --watch --env-file=.env dist/bot.js"
}
} |
|
Скрипт build компилирует TypeScript в JavaScript, start запускает скомпилированный бот, а dev запускает режим разработки с автоматическим перезапуском при изменении кода.
Наконец, создайте файл src/bot.ts с минимальной конфигурацией:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| import { Bot } from 'grammy';
// Проверка наличия токена
const token = process.env.TELEGRAM_BOT_TOKEN;
if (!token) {
throw new Error('TELEGRAM_BOT_TOKEN не указан в переменных окружения');
}
// Создание экземпляра бота
const bot = new Bot(token);
// Обработка команды /start
bot.command('start', (ctx) => {
return ctx.reply('Привет! Я новый бот на grammY.');
});
// Запуск бота
bot.start(); |
|
Такая конфигурация создает минимально работающий бот, который отвечает на команду /start. С этой основой вы готовы к дальнейшему расширению функциональности.
Установка необходимых пакетов
Правильный набор зависимостей — залог успешной разработки Telegram-бота. Хотя мы уже установили базовые пакеты в предыдущем разделе, для создания полноценного бота потребуются дополнительные инструменты. Рассмотрим основные пакеты, которые стоит добавить в проект. Начнем с улучшения работы с переменными окружения. Пакет dotenv мы уже установили, но для TypeScript полезно добавить валидацию конфигурации:
Этот пакет позволяет не только загружать переменные окружения, но и проверять их типы, что предотвращает ошибки конфигурации:
| TypeScript | 1
2
3
4
5
6
| import { str, cleanEnv } from 'envalid';
export const env = cleanEnv(process.env, {
TELEGRAM_BOT_TOKEN: str(),
API_URL: str({ default: 'https://api.example.com' })
}); |
|
Для обработки разных типов медиафайлов часто требуются дополнительные утилиты:
| Bash | 1
| npm install file-type axios form-data |
|
При работе с голосовыми сообщениями и аудиофайлами может потребоваться конвертация форматов:
| Bash | 1
| npm install fluent-ffmpeg @types/fluent-ffmpeg |
|
Для хранения данных сессий и пользовательских настроек существует несколько вариантов:
| Bash | 1
| npm install @grammyjs/storage-file |
|
Или для баз данных:
| Bash | 1
2
3
| npm install mongoose # для MongoDB
# или
npm install pg typeorm # для PostgreSQL с TypeORM |
|
Если планируется создавать сложные диалоги с пользователем:
| Bash | 1
| npm install @grammyjs/conversations |
|
Для логирования событий и отладки:
При разработке ботов с локализацией:
| Bash | 1
| npm install @grammyjs/i18n |
|
Для форматирования и проверки кода:
| Bash | 1
| npm install --save-dev eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin prettier eslint-config-prettier |
|
После установки пакетов рекомендуется создать конфигурационные файлы:
| Bash | 1
2
| npx eslint --init
echo {} > .prettierrc.json |
|
Не забудьте обновить скрипты в package.json для использования новых инструментов:
| JSON | 1
2
3
4
5
6
7
8
9
| {
"scripts": {
"build": "tsc",
"start": "node --env-file=.env dist/bot.js",
"dev": "tsc -w & node --watch --env-file=.env dist/bot.js",
"lint": "eslint . --ext .ts",
"format": "prettier --write \"src/**/*.ts\""
}
} |
|
Для оптимизации веса проекта можно использовать анализатор зависимостей:
| Bash | 1
| npm install --save-dev depcheck |
|
И добавить соответствующий скрипт:
| JSON | 1
2
3
4
5
6
| {
"scripts": {
// ...другие скрипты
"depcheck": "depcheck"
}
} |
|
Это поможет выявить неиспользуемые зависимости и избежать разрастания node_modules.
Хотя список пакетов может казаться внушительным, каждый из них решает конкретную задачу. Выбирайте только те, которые действительно необходимы для вашего бота, чтобы избежать избыточной сложности и увеличения времени сборки проекта.
Получение токена от BotFather
Любая разработка Telegram-бота начинается с получения специального токена, который служит ключом доступа к API Telegram. Этот токен выдаётся только через официального бота Telegram под названием BotFather, который выполняет роль регистратора новых ботов в системе. BotFather – это метабот, созданный командой Telegram специально для управления другими ботами. Он помогает создавать новых ботов, настраивать их внешний вид, команды и многие другие параметры.
Для получения токена выполните следующие шаги:
1. Откройте Telegram и в поисковой строке найдите @BotFather или перейдите напрямую по имени пользователя.
2. Запустите диалог с BotFather, отправив команду /start. В ответ вы получите приветственное сообщение со списком доступных команд.
3. Отправьте команду /newbot для создания нового бота.
4. BotFather попросит вас указать имя вашего бота – это отображаемое имя, которое будут видеть пользователи. Введите любое подходящее имя, например, "Gemini AI Bot".
5. Затем BotFather попросит указать уникальное имя пользователя для бота. Оно должно оканчиваться на "bot" и быть уникальным среди всех ботов Telegram. Например, "gemini01_bot" или придумайте своё.
6. После успешнго создания бота BotFather отправит вам сообщение с токеном доступа. Он выглядит примерно так: 123456789:ABCDefGhIJKlmNoPQRsTUVwxyZ.
Этот токен – ключевой элемент безопасности вашего бота. С его помощью можно полностью управлять ботом, поэтому никогда не публикуйте его в открытом доступе. Храните токен в файле .env, который добавлен в .gitignore:
| TypeScript | 1
| TELEGRAM_BOT_TOKEN=123456789:ABCDefGhIJKlmNoPQRsTUVwxyZ |
|
BotFather также предоставляет множество команд для настройки вашего бота:
/setdescription – установка описания бота,
/setabouttext – информация о боте в профиле,
/setuserpic – загрузка аватара бота,
/setcommands – определение списка команд,
/setprivacy – настройка приватности.
Для удобства взаимодействия с пользователями рекомендуется сразу настроить команды и описание. Это повышает юзабилити бота и упрощает знакомство с его функциями.
Полученный токен будет использоваться при инициализации экземпляра бота в нашем коде:
| TypeScript | 1
2
3
| import { Bot } from 'grammy';
const bot = new Bot(process.env.TELEGRAM_BOT_TOKEN!); |
|
С полученным токеном вы готовы перейти к разработке функциональности вашего бота. Помните, что для каждого нового бота требуется получение отдельного токена.
Настройка TypeScript для оптимальной работы с grammY
TypeScript предоставляет мощный инструментарий для разработки надежных приложений, но для достижения максимальной эффективности при работе с grammY требуется правильная настройка компилятора. Создав оптимальную конфигурацию TypeScript, вы получите не только типобезопасность, но и улучшенную производительность разработки. Центральным элементом конфигурации TypeScript является файл tsconfig.json. Для работы с grammY рекомендуется следующая базовая конфигурация:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| {
"compilerOptions": {
"target": "ES2020",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"outDir": "dist",
"sourceMap": true,
"noImplicitAny": true,
"strictNullChecks": true,
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
} |
|
Рассмотрим ключевые параметры и их влияние на процесс разработки:
"target": "ES2020" – определяет версию JavaScript, в которую будет скомпилирован код. ES2020 обеспечивает поддержку современных возможностей, включая опциональную цепочку вызовов (?.), что особенно полезно при работе с объектами контекста в grammY:
| TypeScript | 1
2
| // Безопасное обращение к вложенным свойствам
const username = ctx.message?.from?.username ?? "неизвестный пользователь"; |
|
"module" и "moduleResolution": "NodeNext" позволяют использовать современную систему модулей ES с полной поддержкой импортов пакетов из Node.js.
"strict": true включает строгий режим типизации, что критически важно для выявления потенциальных ошибок на этапе компиляции. GrammY значительно выигрывает от строгой типизации, поскольку многие ошибки в обработке сообщений можно выявить еще до запуска бота.
"strictNullChecks": true заставляет компилятор внимательно отслеживать возможные null и undefined значения. Это особенно полезно при работе с опциональными полями в сообщениях Telegram:
| TypeScript | 1
2
3
4
5
6
| bot.on("message:text", (ctx) => {
// Компилятор потребует проверки, если strictNullChecks включен
if (ctx.from) {
console.log(`Сообщение от ${ctx.from.first_name}`);
}
}); |
|
"resolveJsonModule": true позволяет импортировать JSON-файлы как модули, что удобно для хранения конфигураций или локализаций. Для увеличения производительности при разработке также рекомендуется настроить инкрементальную компиляцию:
| TypeScript | 1
2
3
4
5
6
7
| {
"compilerOptions": {
// ... другие настройки
"incremental": true,
"tsBuildInfoFile": "./dist/.tsbuildinfo"
}
} |
|
Это позволит компилятору перекомпилировать только измененные файлы, что значительно ускоряет процесс разработки в больших проектах.
При работе с grammY полезно настроить типы сессий и сохраняемых данных. Для этого создайте файл с объявлениями типов, например types.d.ts:
| TypeScript | 1
2
3
4
5
6
7
8
9
| declare module "grammy" {
interface Context {
session: {
userId?: number;
state: "idle" | "awaiting_input" | "processing";
data: Record<string, any>;
};
}
} |
|
Такой подход позволяет расширить существующие типы Context из grammY вашими собственными данными, делая работу с сессиями типобезопасной.
Для дополнительной защиты от ошибок можно активировать строгую проверку свойств:
| TypeScript | 1
2
3
4
5
6
7
| {
"compilerOptions": {
// ... другие настройки
"strictPropertyInitialization": true,
"noUncheckedIndexedAccess": true
}
} |
|
Это предотвратит ошибки, связанные с неинициализированными свойствами классов и небезопасным доступом к индексированным типам – частый источник ошибок при работе с данными из Telegram API.
Структура .env файла и работа с переменными окружения
Файл .env служит центральным хранилищем конфигурационных данных, которые не должны попадать в систему контроля версий, таких как токены доступа, ключи API и другие чувствительные параметры. Базовая структура .env файла для проекта бота на grammY может выглядеть следующим образом:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| # Основные настройки бота
TELEGRAM_BOT_TOKEN=123456789:ABCDefGhIJKlmNoPQRsTUVwxyZ
BOT_MODE=development # development, production или testing
# Настройки API
API_URL=https://api.example.com
API_KEY=your_api_key_here
# Настройки базы данных
DB_HOST=localhost
DB_PORT=27017
DB_NAME=telegram_bot
DB_USER=user
DB_PASSWORD=password
# Настройки логирования
LOG_LEVEL=info
# Другие сервисы
GEMINI_API_KEY=your_gemini_api_key |
|
При именовании переменных окружения существует несколько конвенций, которых стоит придерживаться: использовать ЗАГЛАВНЫЕ_БУКВЫ_С_ПОДЧЕРКИВАНИЯМИ, группировать связанные переменные общим префиксом (например, DB_*) и давать описательные имена. Для загрузки переменных из .env файла в Node.js приложение используется пакет dotenv. Однако при работе с TypeScript рекомендуется добавить типизацию и валидацию переменных окружения. Для этого отлично подходит пакет envalid:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| // src/config.ts
import { str, bool, port, url, makeValidator, cleanEnv } from 'envalid';
// Создаем собственный валидатор для режима работы бота
const mode = makeValidator((x) => {
const modes = ['development', 'production', 'testing'];
if (modes.includes(x)) return x;
throw new Error(`Ожидаемые режимы: ${modes.join(', ')}`);
});
// Определяем и валидируем структуру переменных окружения
export const env = cleanEnv(process.env, {
TELEGRAM_BOT_TOKEN: str(),
BOT_MODE: mode({ default: 'development' }),
API_URL: url({ default: undefined, desc: 'URL внешнего API' }),
API_KEY: str({ default: undefined }),
DB_HOST: str({ default: 'localhost' }),
DB_PORT: port({ default: 27017 }),
DB_NAME: str(),
DB_USER: str({ default: '' }),
DB_PASSWORD: str({ default: '' }),
LOG_LEVEL: str({ choices: ['error', 'warn', 'info', 'debug'], default: 'info' }),
GEMINI_API_KEY: str({ default: undefined })
}); |
|
Такой подход дает несколько преимуществ: автоматическая валидация при запуске приложения, типизация значений (TypeScript знает, что env.DB_PORT — это число), установка значений по умолчанию и документирование назначения переменных.
Для разных сред разработки рекомендуется использовать разные файлы: .env.development, .env.production и .env.testing. Выбор нужного файла можно автоматизировать на основе переменной NODE_ENV:
| TypeScript | 1
2
3
4
5
6
7
8
| // Загрузка соответствующего .env файла
import * as dotenv from 'dotenv';
const envFile = process.env.NODE_ENV
? `.env.${process.env.NODE_ENV}`
: '.env';
dotenv.config({ path: envFile }); |
|
Для улучшения безопасности стоит следовать нескольким правилам:- Всегда добавляйте
.env* файлы в .gitignore.
- Создавайте шаблон
.env.example с примерами значений, но без реальных данных.
- Не храните секреты в коде или коммитах.
- Регулярно меняйте токены и пароли.
Использование переменных окружения в коде бота должно происходить через центральный конфигурационный модуль, а не напрямую через process.env:
| TypeScript | 1
2
3
4
5
6
| // Неправильно
const bot = new Bot(process.env.TELEGRAM_BOT_TOKEN!);
// Правильно
import { env } from './config';
const bot = new Bot(env.TELEGRAM_BOT_TOKEN); |
|
Такой подход обеспечивает типобезопасность, централизованную валидацию и документирование конфигурации, что критически важно для поддерживаемых и масштабируемых ботов.
Настройка ESLint и Prettier для поддержания качества кода
Разработка Telegram-бота, как и любого другого TypeScript-проекта, требует не только функциональности, но и поддержания высокого качества кода. Два инструмента, которые помогают в этом — ESLint для анализа кода и выявления потенциальных проблем, и Prettier для автоматического форматирования. Начнем с установки необходимых пакетов:
| Bash | 1
2
| npm install --save-dev eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin
npm install --save-dev prettier eslint-config-prettier eslint-plugin-prettier |
|
Пакеты eslint-config-prettier и eslint-plugin-prettier помогают ESLint и Prettier работать вместе без конфликтов: первый отключает правила ESLint, которые могут конфликтовать с Prettier, а второй позволяет запускать Prettier как плагин ESLint.
После установки создайте файл конфигурации ESLint .eslintrc.js:
| JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin', 'prettier'],
extends: [
'plugin:@typescript-eslint/recommended',
'prettier',
'plugin:prettier/recommended',
],
root: true,
env: {
node: true,
},
rules: {
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'warn',
'prettier/prettier': 'error',
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
},
}; |
|
Затем добавьте конфигурацию Prettier в файл .prettierrc:
| JSON | 1
2
3
4
5
6
7
8
9
| {
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100,
"tabWidth": 2,
"semi": true,
"bracketSpacing": true,
"arrowParens": "avoid"
} |
|
Для игнорирования определенных файлов при проверке и форматировании создайте файлы .eslintignore и .prettierignore:
| TypeScript | 1
2
3
4
| # .eslintignore и .prettierignore могут иметь одинаковое содержимое
node_modules
dist
.env* |
|
Для удобного запуска проверок добавьте скрипты в package.json:
| JSON | 1
2
3
4
5
6
| {
"scripts": {
"lint": "eslint \"src/**/*.ts\" --fix",
"format": "prettier --write \"src/**/*.ts\""
}
} |
|
Эти настройки позволят автоматически форматировать код и выявлять потенциальные проблемы. Например, ESLint поможет найти неиспользуемые переменные, уязвимости и неправильное использование API, а Prettier обеспечит единый стиль кода во всем проекте.
Для максимальной эффективности рекомендуется настроить интеграцию ESLint и Prettier с вашей средой разработки. Например, VS Code имеет расширения для обоих инструментов, которые позволяют форматировать код при сохранении файла:
| JSON | 1
2
3
4
5
6
7
8
| // .vscode/settings.json
{
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"eslint.validate": ["typescript"]
} |
|
Благодаря этим настройкам ваш код Telegram-бота будет всегда чистым, последовательным и соответствовать лучшим практикам TypeScript. Это особенно важно при работе в команде или при открытой разработке, когда множество разработчиков может вносить изменения в код.
Базовая структура бота
После настройки необходимого окружения и установки пакетов пришло время сформировать базовую структуру бота. Правильная организация файлов и компонентов с самого начала значительно упростит дальнейшую разработку и масштабирование проекта. Для небольших ботов достаточно простой структуры файлов, но для более сложных проектов рекомендуется модульный подход. Оптимальная базовая структура может выглядеть следующим образом:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| telegram-bot/
├── src/
│ ├── bot.ts # Точка входа и инициализация бота
│ ├── config.ts # Конфигурация и переменные окружения
│ ├── handlers/ # Обработчики команд и сообщений
│ │ ├── commands.ts # Обработчики команд (/start, /help и т.д.)
│ │ ├── messages.ts # Обработчики текстовых сообщений
│ │ └── media.ts # Обработчики медиафайлов (фото, голос)
│ ├── middleware/ # Промежуточные обработчики
│ │ ├── logger.ts # Логирование запросов
│ │ └── session.ts # Управление сессиями пользователей
│ ├── services/ # Внешние сервисы и API
│ │ └── gemini.ts # Интеграция с Google Gemini API
│ ├── types/ # Типы и интерфейсы
│ │ └── index.ts # Общие типы для проекта
│ └── utils/ # Вспомогательные функции
│ └── formatter.ts # Форматирование сообщений
├── .env # Переменные окружения
└── package.json |
|
Основой любого бота является файл bot.ts, который содержит инициализацию экземпляра бота и подключение обработчиков. Минимальная версия этого файла выглядит так:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| import { Bot } from 'grammy';
import { env } from './config';
// Создание экземпляра бота
const bot = new Bot(env.TELEGRAM_BOT_TOKEN);
// Обработчик команды /start
bot.command('start', async (ctx) => {
const userName = ctx.from?.first_name || 'пользователь';
await ctx.reply(`Привет, ${userName}! Я бот на grammY.`);
});
// Обработчик текстовых сообщений
bot.on('message:text', async (ctx) => {
await ctx.reply('Вы отправили текстовое сообщение!');
});
// Обработка ошибок
bot.catch((err) => {
console.error('Ошибка в работе бота:', err);
});
// Запуск бота
bot.start(); |
|
При масштабировании проекта имеет смысл выносить обработчики в отдельные модули. Например, файл handlers/commands.ts может содержать:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
| import { Bot } from 'grammy';
export function setupCommandHandlers(bot: Bot) {
bot.command('start', async (ctx) => {
// Обработка команды /start
});
bot.command('help', async (ctx) => {
// Обработка команды /help
});
// Другие обработчики команд
} |
|
А затем в основном файле bot.ts подключать модуль:
| TypeScript | 1
2
3
4
| import { setupCommandHandlers } from './handlers/commands';
// Подключение обработчиков
setupCommandHandlers(bot); |
|
Такая модульная структура позволяет избежать разрастания главного файла и упрощает поддержку кода. По мере усложнения бота вы можете добавлять новые модули или реструктурировать существующие без значительных изменений в основной логике.
Инициализация проекта
После настройки окружения следующим шагом будет инициализация самого проекта бота. Этот процесс закладывает основу всего приложения и определяет его последующую структуру и масштабируемость.
Начнем с создания точки входа для нашего бота. В корне директории src создадим файл index.ts, который будет служить запускающим модулем:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| import { Bot } from 'grammy';
import { env } from './config';
import { setupBot } from './bot';
// Асинхронная функция запуска для корректной обработки ошибок
async function bootstrap() {
try {
// Создание экземпляра бота с токеном из переменных окружения
const bot = new Bot(env.TELEGRAM_BOT_TOKEN);
// Настройка основных компонентов бота
setupBot(bot);
// Запуск бота
console.log('Бот запускается...');
await bot.start();
} catch (error) {
console.error('Ошибка при запуске бота:', error);
process.exit(1);
}
}
// Запуск приложения
bootstrap(); |
|
Теперь создадим файл bot.ts, который будет содержать основную логику настройки бота:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| import { Bot } from 'grammy';
import { setupCommandHandlers } from './handlers/commands';
import { setupMessageHandlers } from './handlers/messages';
import { setupMiddleware } from './middleware';
export function setupBot(bot: Bot) {
// Подключение middleware (логгеры, обработчики сессий и т.д.)
setupMiddleware(bot);
// Регистрация обработчиков команд и сообщений
setupCommandHandlers(bot);
setupMessageHandlers(bot);
// Обработчик ошибок
bot.catch((err) => {
const ctx = err.ctx;
console.error(`Ошибка при обработке обновления ${ctx.update.update_id}:`, err.error);
ctx.reply('Произошла ошибка при обработке вашего запроса').catch(console.error);
});
} |
|
Функциональные модули должны быть распределены по соответствующим директориям. Например, создадим базовую структуру обработчиков команд в файле handlers/commands.ts:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| import { Bot } from 'grammy';
export function setupCommandHandlers(bot: Bot) {
// Обработка команды /start
bot.command('start', async (ctx) => {
const userName = ctx.from?.first_name || 'пользователь';
await ctx.reply(`Здравствуйте, ${userName}! Я бот на платформе grammY.`);
});
// Обработка команды /help
bot.command('help', async (ctx) => {
await ctx.reply('Доступные команды:\n/start - Начало работы\n/help - Справка');
});
} |
|
Для запуска проекта в режиме разработки необходимо настроить скрипты в package.json:
| JSON | 1
2
3
4
5
6
7
8
| {
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "nodemon --exec ts-node src/index.ts",
"lint": "eslint "src/**/*.ts" --fix"
}
} |
|
Дополнительно установим пакет nodemon для автоматического перезапуска приложения при изменении кода:
| Bash | 1
| npm install --save-dev nodemon ts-node |
|
Такая структура инициализации проекта обеспечивает ряд преимуществ:- Модульность и разделение ответственности между компонентами.
- Упрощенное тестирование отдельных модулей.
- Легкость добавления новых функций без изменения существующего кода.
- Более простая отладка благодаря изолированным компонентам.
При запуске проекта через команду npm run dev бот начнет работу в режиме разработки с автоматическим перезапуском при внесении изменений в код, что значительно ускоряет процесс разработки и отладки.
Настройка основных файлов и конфигураций
После инициализации проекта необходимо настроить основные файлы и конфигурации, которые составят фундамент нашего бота. Эта настройка включает создание ключевых файлов и определение их структуры для обеспечения эффективной работы приложения. Прежде всего, создадим файл конфигурации, который будет централизованно управлять всеми параметрами приложения. В директории src создадим файл config.ts:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| import { cleanEnv, str, bool, num } from 'envalid';
// Валидация переменных окружения
export const env = cleanEnv(process.env, {
TELEGRAM_BOT_TOKEN: str(),
NODE_ENV: str({ choices: ['development', 'production', 'test'], default: 'development' }),
DEBUG: bool({ default: false }),
PORT: num({ default: 3000 }), // Для webhook сервера, если требуется
GEMINI_API_KEY: str({ default: undefined })
});
// Конфигурация приложения
export const config = {
bot: {
token: env.TELEGRAM_BOT_TOKEN,
options: {
polling: env.NODE_ENV !== 'production' // В разработке используем long polling
}
},
gemini: {
apiKey: env.GEMINI_API_KEY,
model: 'gemini-1.5-flash'
},
logging: {
level: env.NODE_ENV === 'production' ? 'info' : 'debug'
}
}; |
|
Этот файл не только загружает и валидирует переменные окружения, но и предоставляет структурированный доступ к настройкам всего приложения. Далее, создадим основные директории и файлы для типов данных. В папке src/types создадим файл index.ts:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| // Типы для работы с сессиями пользователей
export interface SessionData {
state: 'idle' | 'awaiting_input' | 'processing';
userData?: {
name?: string;
preferences?: Record<string, any>;
};
conversationHistory: {
role: 'user' | 'bot';
message: string;
timestamp: number;
}[];
}
// Расширение типов grammY
declare global {
namespace GrammyTypes {
interface Context {
session: SessionData;
}
}
} |
|
Для обработки ошибок создадим специальный файл src/utils/error-handler.ts:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| import { BotError, Context, GrammyError, HttpError } from 'grammy';
import { config } from '../config';
export async function errorHandler(err: BotError<Context>): Promise<void> {
const { ctx, error } = err;
console.error(`Ошибка при обработке обновления ${ctx.update.update_id}:`);
if (error instanceof GrammyError) {
// Ошибки Telegram API
console.error('Ошибка API Telegram:', error.description);
} else if (error instanceof HttpError) {
// Ошибки сети
console.error('Ошибка сети:', error);
} else {
// Непредвиденные ошибки
console.error('Неизвестная ошибка:', error);
}
// Уведомляем пользователя только в режиме разработки
if (config.logging.level === 'debug') {
try {
await ctx.reply('Произошла ошибка при обработке вашего запроса. Пожалуйста, попробуйте позже.');
} catch (e) {
console.error('Не удалось отправить сообщение об ошибке пользователю:', e);
}
}
} |
|
Чтобы структурировать взаимодействие с Google Gemini API, создадим сервисный файл src/services/gemini-service.ts:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| import { GoogleGenerativeAI } from '@google/generative-ai';
import { config } from '../config';
export class GeminiService {
private genAI: GoogleGenerativeAI;
private model: any;
constructor() {
this.genAI = new GoogleGenerativeAI(config.gemini.apiKey!);
this.model = this.genAI.getGenerativeModel({
model: config.gemini.model,
systemInstruction: 'Ты Telegram-бот, помогающий пользователям. Отвечай кратко и информативно.'
});
}
async generateTextResponse(prompt: string): Promise<string> {
try {
const result = await this.model.generateContent(prompt);
return result.response.text();
} catch (error) {
console.error('Ошибка при генерации ответа:', error);
return 'К сожалению, не удалось сгенерировать ответ. Попробуйте позже.';
}
}
} |
|
Эти файлы формируют базовую инфраструктуру проекта, которая отвечает за конфигурацию, обработку ошибок и взаимодействие с внешними сервисами. Такая организация кода обеспечивает ясность структуры и упрощает дальнейшую разработку и поддержку бота.
Организация кода по принципу модульности
Модульная архитектура — это подход, при котором функциональность приложения разделяется на отдельные, самодостаточные компоненты (модули), каждый из которых отвечает за определённую часть бизнес-логики. В контексте бота на grammY модульность позволяет разделить код на логические блоки, упрощая его понимание, тестирование и дальнейшее расширение. Правильно организованная модульная структура особенно полезна, когда над проектом работает команда разработчиков или когда функциональность бота со временем расширяется. Основная идея модульности состоит в том, чтобы группировать код не по типам файлов, а по функциональным возможностям бота. Например, вместо хранения всех обработчиков команд в одном большом файле, разделим их по функциональным модулям:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| src/
├── modules/
│ ├── profile/ # Модуль профиля пользователя
│ │ ├── handlers.ts # Обработчики команд профиля
│ │ ├── services.ts # Сервисы для работы с данными профиля
│ │ └── types.ts # Типы данных профиля
│ ├── orders/ # Модуль для работы с заказами
│ │ ├── handlers.ts # Обработчики команд заказов
│ │ ├── services.ts # Бизнес-логика заказов
│ │ └── types.ts # Типы данных заказов
│ └── weather/ # Модуль погоды
│ ├── handlers.ts # Обработчики погодных команд
│ ├── api.ts # Взаимодействие с погодным API
│ └── formatter.ts # Форматирование погодной информации |
|
Каждый модуль должен иметь четко определенный общедоступный интерфейс, который скрывает детали реализации. Например, файл index.ts в корне модуля может экспортировать только нужные функции:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| // modules/profile/index.ts
import { Bot } from 'grammy';
import { profileHandlers } from './handlers';
export function setupProfileModule(bot: Bot) {
// Подключение всех обработчиков модуля к боту
bot.command('profile', profileHandlers.showProfile);
bot.command('edit', profileHandlers.editProfile);
// Другая инициализация модуля
return {
// Публичные методы модуля, которые могут использоваться другими частями приложения
getUserProfile: profileHandlers.getUserProfile
};
} |
|
Такой подход обеспечивает инкапсуляцию внутренней логики модуля и предоставляет чистый интерфейс для взаимодействия с ним.
В основном файле бота подключение модулей будет выглядеть просто и элегантно:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| // src/bot.ts
import { Bot } from 'grammy';
import { setupProfileModule } from './modules/profile';
import { setupOrdersModule } from './modules/orders';
import { setupWeatherModule } from './modules/weather';
export function setupBot(bot: Bot) {
// Подключение всех модулей
setupProfileModule(bot);
setupOrdersModule(bot);
setupWeatherModule(bot);
// Общие обработчики, не привязанные к конкретному модулю
bot.command('start', ctx => ctx.reply('Добро пожаловать!'));
bot.command('help', ctx => ctx.reply('Справка по командам...'));
return bot;
} |
|
Такая структура значительно упрощает навигацию по коду проекта — когда нужно внести изменения в функциональность профиля, разработчику не нужно искать соответствующий код среди множества других обработчиков.
Ещё одно преимущество модульного подхода — возможность условного подключения модулей в зависимости от конфигурации:
| TypeScript | 1
2
3
4
5
6
7
8
| // Пример условного подключения модулей
if (config.features.weatherEnabled) {
setupWeatherModule(bot);
}
if (config.features.ordersEnabled) {
setupOrdersModule(bot);
} |
|
Модульность также способствует повторному использованию кода. Хорошо спроектированный модуль может быть использован в нескольких проектах с минимальными изменениями. Например, модуль аутентификации может применяться во многих ботах с похожей функциональностью. При разработке модулей следует придерживаться принципа единственной ответственности, когда каждый модуль отвечает только за одну функциональную область. Это делает код более понятным и облегчает его поддержку в долгосрочной перспективе.
Использование интерфейсов TypeScript для типизации данных бота
Одной из сильнейших сторон TypeScript при разработке Telegram-ботов является возможность создания строгой типизации для всех компонентов приложения. Интерфейсы позволяют однозначно определять структуру данных, с которыми работает бот, что значительно снижает вероятность ошибок и повышает качество кода. При разработке бота на grammY мы сталкиваемся с различными типами данных: сообщения от пользователей, информация о чатах, состояния диалогов, настройки пользователей и многое другое. Для каждого из этих типов можно создать соответствующий интерфейс:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // Модель пользователя в нашей системе
interface User {
telegramId: number;
username?: string;
firstName: string;
lastName?: string;
registeredAt: Date;
preferences: UserPreferences;
isAdmin: boolean;
}
// Настройки пользователя
interface UserPreferences {
language: 'ru' | 'en' | 'es';
notifications: boolean;
theme: 'light' | 'dark' | 'system';
} |
|
Особенно полезны интерфейсы при работе с контекстом сообщений в grammY. Контекст содержит информацию о текущем взаимодействии, включая данные о пользователе, сообщении и методы для ответа. Чтобы расширить встроенный тип контекста, можно использовать декларации модулей:
| TypeScript | 1
2
3
4
5
6
7
8
| // Расширение типа Context для пользовательских данных
declare module 'grammy' {
interface Context {
// Добавление пользовательских свойств к контексту
dbUser: User | null;
session: UserSession;
}
} |
|
Такое расширение позволяет безопасно обращаться к пользовательским свойствам в обработчиках:
| TypeScript | 1
2
3
4
5
6
7
8
9
| bot.on('message', async (ctx) => {
// TypeScript знает, что ctx.dbUser имеет тип User | null
if (!ctx.dbUser) {
return ctx.reply('Пожалуйста, зарегистрируйтесь!');
}
// Безопасный доступ к свойствам
await ctx.reply(`Привет, ${ctx.dbUser.firstName}!`);
}); |
|
Для типизации состояний сессии пользователя, что критично для ботов с многоступенчатыми диалогами, можно создать специальные интерфейсы:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
| // Возможные состояния диалога
type DialogState = 'idle' | 'awaitingName' | 'awaitingEmail' | 'awaitingConfirmation';
// Структура сессии пользователя
interface UserSession {
state: DialogState;
formData: {
name?: string;
email?: string;
};
lastActivity: Date;
} |
|
При работе с внешними API, например, с Google Gemini, также полезно определять типы для запросов и ответов:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| // Типы для работы с Gemini API
interface GeminiRequest {
prompt: string;
maxTokens?: number;
temperature?: number;
}
interface GeminiResponse {
text: string;
usage: {
promptTokens: number;
completionTokens: number;
totalTokens: number;
};
}
// Сервис для работы с API
class GeminiService {
async generateResponse(request: GeminiRequest): Promise<GeminiResponse> {
// Реализация запроса к API
}
} |
|
Для обработки различных типов сообщений Telegram можно использовать объединения типов и дженерики:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
| // Обработчик для различных типов сообщений
function processMessage<T extends 'text' | 'photo' | 'voice'>(
type: T,
handler: (ctx: Context & { message: MessageByType<T> }) => Promise<void>
) {
return handler;
}
// Использование типизированного обработчика
const textHandler = processMessage('text', async (ctx) => {
// ctx.message типизирован как текстовое сообщение
const text = ctx.message.text; // Безопасный доступ
}); |
|
В сложных ботах с несколькими модулями типизация помогает четко определять границы между компонентами через интерфейсы:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| // Интерфейс для модуля бота
interface BotModule {
name: string;
setup(bot: Bot): void;
shutdown?(): Promise<void>;
getPublicApi(): Record<string, unknown>;
}
// Реализация модуля
class ProfileModule implements BotModule {
name = 'profile';
setup(bot: Bot): void {
// Настройка обработчиков
}
getPublicApi() {
return {
getUserProfile: this.getUserProfile.bind(this)
};
}
private getUserProfile(userId: number): Promise<User | null> {
// Реализация
}
} |
|
Использование интерфейсов TypeScript делает код бота более надежным, самодокументируемым и облегчает его поддержку в долгосрочной перспективе. Особенно это важно при командной разработке, когда четкие контракты между компонентами системы снижают вероятность интеграционных проблем и ускоряют разработку.
Применение паттерна Dependency Injection в структуре бота
При создании сложных Telegram-ботов вопросы архитектуры и организации кода становятся критически важными. Одним из эффективных подходов к построению гибкой и поддерживаемой системы является применение паттерна Dependency Injection (внедрение зависимостей). Этот архитектурный подход помогает создавать компоненты с низкой связностью, что делает код более тестируемым и модифицируемым. Суть паттерна DI заключается в том, что компонент не создаёт свои зависимости самостоятельно, а получает их извне — чаще всего через конструктор, метод или свойство. В контексте разработки бота на grammY и TypeScript, этот подход особенно полезен для управления сервисами, репозиториями данных и другими вспомогательными классами.
Базовая реализация DI может выглядеть следующим образом:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
| // Интерфейс для работы с хранилищем пользователей
interface UserRepository {
findById(id: number): Promise<User | null>;
save(user: User): Promise<void>;
}
// Реализация хранилища с использованием MongoDB
class MongoUserRepository implements UserRepository {
constructor(private readonly connection: any) {}
async findById(id: number): Promise<User | null> {
// Реализация поиска в MongoDB
}
async save(user: User): Promise<void> {
// Реализация сохранения в MongoDB
}
}
// Сервис, работающий с пользователями
class UserService {
constructor(private readonly userRepository: UserRepository) {}
async getUserProfile(id: number): Promise<UserProfile | null> {
const user = await this.userRepository.findById(id);
if (!user) return null;
return {
id: user.id,
name: user.name,
registrationDate: user.createdAt
};
}
}
// Обработчик команд профиля
class ProfileHandler {
constructor(private readonly userService: UserService) {}
async handleProfileCommand(ctx: Context): Promise<void> {
const userId = ctx.from?.id;
if (!userId) return;
const profile = await this.userService.getUserProfile(userId);
if (!profile) {
await ctx.reply('Профиль не найден. Пожалуйста, зарегистрируйтесь.');
return;
}
await ctx.reply(`Профиль: ${profile.name}`);
}
} |
|
Преимущество такого подхода в том, что мы можем легко заменить реализацию любого компонента, не влияя на другие части системы. Например, если мы решим перейти с MongoDB на PostgreSQL, нам достаточно создать новую реализацию UserRepository и внедрить её в существующие сервисы. Для более сложных проектов стоит рассмотреть использование специализированных контейнеров внедрения зависимостей, таких как tsyringe, inversify или typedi:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| import { injectable, inject, container } from 'tsyringe';
@injectable()
class GeminiService {
constructor(
@inject('ApiKey') private apiKey: string,
@inject('ApiUrl') private apiUrl: string
) {}
async generateResponse(prompt: string): Promise<string> {
// Реализация запроса к Gemini API
}
}
@injectable()
class MessageHandler {
constructor(
@inject(GeminiService) private geminiService: GeminiService
) {}
async handleMessage(ctx: Context): Promise<void> {
const response = await this.geminiService.generateResponse(ctx.message.text);
await ctx.reply(response);
}
}
// Регистрация зависимостей в контейнере
container.register('ApiKey', { useValue: process.env.GEMINI_API_KEY });
container.register('ApiUrl', { useValue: 'https://api.gemini.com/v1' });
container.register(GeminiService, { useClass: GeminiService });
// Получение обработчика с внедренными зависимостями
const messageHandler = container.resolve(MessageHandler);
// Использование в grammY
bot.on('message:text', (ctx) => messageHandler.handleMessage(ctx)); |
|
При разработке бота с использованием DI, важно придерживаться следующих принципов:
1. Инверсия зависимостей: компоненты должны зависеть от абстракций (интерфейсов), а не от конкретных реализаций.
2. Единая ответственность: каждый класс должен выполнять только одну функцию, что упрощает тестирование и модификацию.
3. Разделение конфигурации и использования: настройка зависимостей должна происходить в специальном месте (композиционный корень), отдельно от бизнес-логики.
Такой подход особенно полезен при разработке ботов с комплексной логикой, множеством внешних интеграций или необходимостью тщательного тестирования. Например, для интеграционных тестов мы можем легко заменить реальные реализации сервисов на моки:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| // Тестирование обработчика профиля
describe('ProfileHandler', () => {
it('should show profile info when user exists', async () => {
// Создаем мок-реализацию сервиса
const mockUserService = {
getUserProfile: jest.fn().mockResolvedValue({ id: 1, name: 'Тест' })
};
// Создаем обработчик с мок-сервисом
const handler = new ProfileHandler(mockUserService as any);
// Создаем мок-контекст grammY
const mockCtx = {
from: { id: 1 },
reply: jest.fn()
};
// Вызываем тестируемый метод
await handler.handleProfileCommand(mockCtx as any);
// Проверяем, что был вызван правильный метод ответа
expect(mockCtx.reply).toHaveBeenCalledWith('Профиль: Тест');
});
}); |
|
Внедрение зависимостей в архитектуре Telegram-бота на TypeScript позволяет создавать масштабируемые, поддерживаемые и легко тестируемые приложения. Этот паттерн особенно ценен при развитии проекта, когда требования и технические решения могут меняться со временем.
Стратегии обработки ошибок в TypeScript-боте
Разработка надежного Telegram-бота неизбежно включает продуманную стратегию обработки ошибок. В продуктовой среде ошибки могут возникать по разным причинам: нестабильное соединение с интернетом, сбои в API Telegram, неправильные данные от пользователей или баги в коде. Хорошо спроектированная система обработки исключений не только улучшает пользовательский опыт, но и значительно упрощает отладку и поддержку бота. TypeScript предоставляет мощные возможности для типизированной обработки ошибок, которые особенно полезны при создании надежных ботов. Библиотека grammY включает встроенные механизмы работы с ошибками, которые можно дополнить собственными решениями.
Основные типы ошибок, возникающие при работе бота:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| // Ошибки, возвращаемые API Telegram
class TelegramError extends Error {
constructor(public code: number, message: string) {
super(message);
}
}
// Ошибки бизнес-логики приложения
class BotLogicError extends Error {
constructor(public reason: string, public userId: number) {
super(`Ошибка бизнес-логики: ${reason}`);
}
}
// Ошибки внешних сервисов (например, Gemini API)
class ExternalAPIError extends Error {
constructor(public service: string, public details: any) {
super(`Ошибка в сервисе ${service}`);
}
} |
|
GrammY предоставляет метод bot.catch(), который перехватывает все необработанные ошибки, возникающие при обработке обновлений:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| bot.catch((err) => {
const ctx = err.ctx;
const e = err.error;
console.error(`Ошибка при обработке обновления ${ctx.update.update_id}:`);
if (e instanceof GrammyError) {
// Ошибки, связанные с Telegram API
console.error("Ошибка в запросе к Telegram:", e.description);
} else if (e instanceof HttpError) {
// Ошибки сетевого взаимодействия
console.error("Ошибка сети:", e);
} else if (e instanceof BotLogicError) {
// Наши собственные ошибки бизнес-логики
console.error(`Пользователь ${e.userId} вызвал ошибку: ${e.reason}`);
} else {
// Неизвестные ошибки
console.error("Неизвестная ошибка:", e);
}
// Отправляем пользователю сообщение о проблеме
ctx.reply("Извините, произошла ошибка. Мы уже работаем над её устранением.").catch(e => {
console.error("Не удалось отправить сообщение об ошибке:", e);
});
}); |
|
Для типичных асинхронных операций стоит использовать конструкцию try/catch с информативными сообщениями:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| bot.command("profile", async (ctx) => {
try {
const userId = ctx.from?.id;
if (!userId) {
throw new BotLogicError("Не удалось определить ID пользователя", 0);
}
const profile = await userService.getUserProfile(userId);
await ctx.reply(`Ваш профиль: ${JSON.stringify(profile)}`);
} catch (error) {
// Локальная обработка ошибки
console.error("Ошибка при получении профиля:", error);
// Отправка понятного пользователю сообщения
await ctx.reply("Не удалось загрузить ваш профиль. Пожалуйста, попробуйте позже.");
// Важно: не глотаем ошибку полностью, а пробрасываем её дальше для центральной обработки
throw error;
}
}); |
|
Для отслеживания и анализа ошибок полезно организовать централизованное логирование:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| // services/logger.ts
import winston from 'winston';
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' })
]
});
// В рабочем окружении также выводим логи в консоль
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.simple()
}));
}
export { logger }; |
|
Продвинутой стратегией является создание middleware для обработки специфических ошибок:
| TypeScript | 1
2
3
4
5
6
7
8
9
| // Middleware для отлова и обработки ошибок авторизации
const authErrorHandler = (err: unknown, ctx: Context, next: () => Promise<void>) => {
if (err instanceof AuthError) {
return ctx.reply("Для выполнения этой команды необходима авторизация.");
}
return next();
};
bot.use(authErrorHandler); |
|
Важно также помнить о проблеме "слишком много запросов" (rate limiting) в Telegram API:
| TypeScript | 1
2
3
4
5
6
7
8
| import { limit } from '@grammyjs/ratelimiter';
// Ограничиваем количество сообщений до 3 в секунду на пользователя
bot.use(limit({
timeFrame: 1000,
limit: 3,
onLimitExceeded: (ctx) => ctx.reply("Пожалуйста, не отправляйте сообщения так часто!")
})); |
|
Сочетание типизированных ошибок TypeScript, встроенных механизмов grammY и продуманных стратегий обработки исключений позволяет создавать надежные Telegram-боты, способные грамотно реагировать на различные проблемные ситуации и сохранять положительный пользовательский опыт даже при возникновении ошибок.
Обработка команд и сообщений
В GrammY обработка сообщений осуществляется через систему фильтров и обработчиков. Фильтры определяют, какие сообщения должны быть обработаны конкретным обработчиком, а сами обработчики содержат логику ответа на сообщение.
Базовая структура обработчика выглядит следующим образом:
| TypeScript | 1
2
3
4
| bot.on('<фильтр>', async (ctx) => {
// Логика обработки сообщения
await ctx.reply('Ответ на сообщение');
}); |
|
Для обработки команд (сообщений, начинающихся с символа "/") GrammY предоставляет специальный метод command:
| TypeScript | 1
2
3
4
| bot.command('start', async (ctx) => {
const name = ctx.from?.first_name || 'пользователь';
await ctx.reply(`Здравствуйте, ${name}! Чем могу помочь?`);
}); |
|
При работе с текстовыми сообщениями можно использовать фильтр message:text:
| TypeScript | 1
2
3
4
| bot.on('message:text', async (ctx) => {
const text = ctx.message.text;
await ctx.reply(`Вы написали: ${text}`);
}); |
|
Более сложные шаблоны сообщений можно обрабатывать с помощью метода hears, который поддерживает как строки, так и регулярные выражения:
| TypeScript | 1
2
3
4
5
| // Реакция на конкретную фразу
bot.hears('привет', (ctx) => ctx.reply('И вам привет!'));
// Реакция на сообщения, соответствующие регулярному выражению
bot.hears(/спасибо/i, (ctx) => ctx.reply('Всегда пожалуйста!')); |
|
GrammY позволяет создавать цепочки обработчиков, где каждый обработчик может решить, передавать ли управление следующему в цепочке через функцию next():
| TypeScript | 1
2
3
4
5
6
| bot.use(async (ctx, next) => {
console.log('Получено новое сообщение');
// Передаем управление следующему обработчику
await next();
console.log('Ответ отправлен');
}); |
|
Для работы с различными типами медиафайлов используются соответствующие фильтры:
| TypeScript | 1
2
3
4
5
6
7
8
9
| // Обработка фотографий
bot.on('message:photo', async (ctx) => {
await ctx.reply('Получена фотография!');
});
// Обработка голосовых сообщений
bot.on('message:voice', async (ctx) => {
await ctx.reply('Получено голосовое сообщение!');
}); |
|
Одним из преимуществ TypeScript является автоматический вывод типов контекста в зависимости от фильтра. Например, при использовании фильтра message:text компилятор TypeScript будет знать, что ctx.message содержит текстовое сообщение, и позволит безопасно обращаться к свойству text:
| TypeScript | 1
2
3
4
5
| bot.on('message:text', (ctx) => {
// TypeScript знает, что здесь ctx.message.text доступен
const words = ctx.message.text.split(' ').length;
ctx.reply(`В вашем сообщении ${words} слов`);
}); |
|
Это предотвращает множество потенциальных ошибок при разработке и делает код более надежным.
Реализация ответов на команды
Реализация эффективной системы ответов на команды — важный аспект разработки любого Telegram-бота. Команды в Telegram — это специальные сообщения, начинающиеся с символа "/" (например, /start, /help, /settings), которые представляют собой основной способ взаимодействия пользователя с ботом. GrammY предоставляет удобные механизмы для их обработки. Регистрация обработчиков команд в grammY осуществляется через метод command(), который принимает название команды (без символа "/") и функцию-обработчик:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // Обработка команды /start
bot.command('start', async (ctx) => {
const userName = ctx.from?.first_name || 'пользователь';
await ctx.reply(`Добро пожаловать, ${userName}! Я ваш новый ассистент.`);
});
// Обработка команды /help
bot.command('help', async (ctx) => {
await ctx.reply(`Доступные команды:
/start - Начало работы с ботом
/help - Показать список команд
/settings - Настройки бота
/feedback - Отправить отзыв`);
}); |
|
Для получения параметров, переданных вместе с командой можно использовать свойство ctx.match, которое содержит текст после команды:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
| // Обработка команды с параметрами, например: /search TypeScript грамотная работа
bot.command('search', async (ctx) => {
const query = ctx.match;
if (!query) {
return ctx.reply('Укажите поисковый запрос после команды. Например: /search TypeScript');
}
await ctx.reply(`Выполняю поиск по запросу: ${query}`);
// Дальнейшая логика поиска...
}); |
|
Для более сложной обработки параметров команды можно использовать регулярные выражения:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // Обработка команды вида: /remind 30m Позвонить маме
bot.hears(/^\/remind\s+(\d+)([mhd])\s+(.+)$/, async (ctx) => {
const [, amount, unit, text] = ctx.match!;
let minutes = parseInt(amount);
if (unit === 'h') minutes *= 60;
if (unit === 'd') minutes *= 1440;
await ctx.reply(`Напомню о "${text}" через ${amount}${unit}`);
// Установка напоминания в системе...
setTimeout(() => {
ctx.reply(`Напоминание: ${text}`);
}, minutes * 60 * 1000);
}); |
|
При создании ботов со множеством команд рекомендуется структурировать обработчики команд в отдельные модули. Например, можно создать файл commands.ts, который будет экспортировать функцию настройки всех команд:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| // commands.ts
import { Bot } from 'grammy';
import { setupAdminCommands } from './admin-commands';
export function setupCommands(bot: Bot) {
// Базовые команды
bot.command('start', startHandler);
bot.command('help', helpHandler);
bot.command('settings', settingsHandler);
// Подключение команд администратора для определённых пользователей
setupAdminCommands(bot);
}
async function startHandler(ctx) {
// Логика обработки /start
}
async function helpHandler(ctx) {
// Логика обработки /help
}
async function settingsHandler(ctx) {
// Логика обработки /settings
} |
|
Для улучшения пользовательского опыта полезно настроить отображение доступных команд в интерфейсе Telegram. Это делается через BotFather, но также можно программно указать доступные команды:
| TypeScript | 1
2
3
4
5
6
7
8
9
| // Установка списка команд бота
async function setMyCommands(bot: Bot) {
await bot.api.setMyCommands([
{ command: 'start', description: 'Запустить бота' },
{ command: 'help', description: 'Показать помощь' },
{ command: 'settings', description: 'Настройки бота' },
{ command: 'feedback', description: 'Отправить отзыв' }
]);
} |
|
При обработке команд часто требуется проверка прав пользователя или другие проверки. Удобно реализовать это через middleware:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // Middleware для проверки авторизации перед выполнением команды
function authMiddleware(ctx, next) {
const userId = ctx.from?.id;
if (!userId || !isAuthorizedUser(userId)) {
return ctx.reply('У вас нет прав для выполнения этой команды');
}
return next(); // Передаём управление следующему обработчику
}
// Применение middleware к конкретной команде
bot.command('admin', authMiddleware, (ctx) => {
ctx.reply('Вы вошли в режим администратора');
}); |
|
Важно помнить, что команды в Telegram регистрочувствительны и работают только в начале сообщения. Также при регистрации команд через BotFather существует ограничение в 100 символов для описания команды.
Примеры кода для обработки различных типов сообщений
Telegram-боты могут обрабатывать множество различных типов сообщений: текст, фотографии, видео, документы, стикеры, голосовые сообщения и многое другое. GrammY предоставляет удобные фильтры и механизмы для работы с каждым типом контента. Рассмотрим примеры кода для обработки основных типов сообщений.
Обработка текстовых сообщений с шаблонами
Помимо простой обработки текста, часто требуется реагировать на определенные шаблоны или извлекать данные из сообщений:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| // Извлечение данных из текста с помощью регулярных выражений
bot.hears(/имя: (.+), возраст: (\d+)/i, (ctx) => {
const [, name, age] = ctx.match!;
return ctx.reply(`Привет, ${name}! Тебе ${age} лет.`);
});
// Обработка нескольких вариантов приветствия
bot.hears([/привет/i, /здравствуй/i, /добрый день/i], (ctx) => {
return ctx.reply('И вам здравствуйте!');
});
// Создание "эхо-бота" с модификацией текста
bot.on('message:text', (ctx) => {
const originalText = ctx.message.text;
const modifiedText = originalText
.split('')
.reverse()
.join('');
return ctx.reply(`Ваше сообщение наоборот: ${modifiedText}`);
}); |
|
Обработка фотографий
При получении фотографий можно получить доступ к изображению, его свойствам и обработать данные:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| bot.on('message:photo', async (ctx) => {
// Получаем объект файла (выбираем фото максимального размера)
const photoFileId = ctx.message.photo[ctx.message.photo.length - 1].file_id;
// Получение информации о файле
const fileInfo = await ctx.api.getFile(photoFileId);
// Формирование URL для скачивания
const filePath = fileInfo.file_path;
if (!filePath) return ctx.reply('Не удалось получить доступ к фото.');
const fileUrl = `https://api.telegram.org/file/bot${process.env.TELEGRAM_BOT_TOKEN}/${filePath}`;
// Отправляем пользователю информацию о фото
await ctx.reply(`Получено фото! Размер: ${fileInfo.file_size} байт`);
await ctx.reply(`URL для скачивания: ${fileUrl}`);
// При необходимости можно скачать и обработать фото
// const response = await fetch(fileUrl);
// const buffer = await response.arrayBuffer();
// ... обработка изображения
}); |
|
Обработка голосовых сообщений
Работа с голосовыми сообщениями часто включает их преобразование или анализ:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| bot.on('message:voice', async (ctx) => {
// Получаем идентификатор голосового сообщения
const voiceFileId = ctx.message.voice.file_id;
const duration = ctx.message.voice.duration;
await ctx.reply(`Получено голосовое сообщение длительностью ${duration} секунд.`);
// Получение информации о файле
const fileInfo = await ctx.api.getFile(voiceFileId);
const filePath = fileInfo.file_path;
if (!filePath) return ctx.reply('Не удалось получить доступ к голосовому сообщению.');
// Формирование URL для скачивания
const fileUrl = `https://api.telegram.org/file/bot${process.env.TELEGRAM_BOT_TOKEN}/${filePath}`;
// Скачивание и обработка голосового сообщения
try {
const response = await fetch(fileUrl);
const audioData = await response.arrayBuffer();
// Здесь может быть код для обработки аудио, например,
// преобразование в текст через сервис распознавания речи
await ctx.reply('Обработка голосового сообщения завершена!');
} catch (error) {
console.error('Ошибка при обработке голосового сообщения:', error);
await ctx.reply('Произошла ошибка при обработке голосового сообщения.');
}
}); |
|
Обработка документов и файлов
Telegram позволяет пользователям отправлять документы различных форматов:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| bot.on('message:document', async (ctx) => {
const document = ctx.message.document;
const fileName = document.file_name || 'unnamed_file';
const mimeType = document.mime_type || 'unknown';
await ctx.reply(`Получен документ: ${fileName}\nТип: ${mimeType}`);
// Проверка типа файла и его обработка
if (mimeType.includes('image')) {
await ctx.reply('Это изображение в формате документа.');
} else if (mimeType.includes('pdf')) {
await ctx.reply('Получен PDF-документ.');
} else if (mimeType.includes('text')) {
// Получение и обработка текстового файла
const fileInfo = await ctx.api.getFile(document.file_id);
const filePath = fileInfo.file_path;
if (filePath) {
const fileUrl = `https://api.telegram.org/file/bot${process.env.TELEGRAM_BOT_TOKEN}/${filePath}`;
const response = await fetch(fileUrl);
const text = await response.text();
// Отправляем первые 100 символов содержимого
await ctx.reply(`Содержимое файла (первые 100 символов):\n${text.slice(0, 100)}...`);
}
}
}); |
|
Обработка местоположения
Пользователи могут отправлять свое местоположение, которое можно использовать для предоставления контекстных услуг:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| bot.on('message:location', async (ctx) => {
const { latitude, longitude } = ctx.message.location;
await ctx.reply(`Получены координаты: ${latitude}, ${longitude}`);
// Пример использования координат для определения погоды
try {
// Здесь может быть запрос к API погоды
const weather = await getWeatherByCoordinates(latitude, longitude);
await ctx.reply(`Погода в вашем местоположении: ${weather}`);
} catch (error) {
await ctx.reply('Не удалось получить информацию о погоде.');
}
});
// Фиктивная функция для примера
async function getWeatherByCoordinates(lat: number, lon: number): Promise<string> {
// В реальном приложении здесь был бы запрос к погодному API
return "Солнечно, +25°C";
} |
|
Комбинирование обработчиков
В более сложных сценариях может потребоваться комбинированная обработка разных типов сообщений:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| // Общий обработчик для всех типов медиафайлов
bot.on(['message:photo', 'message:video', 'message:document'], async (ctx) => {
let mediaType = 'неизвестный тип';
if ('photo' in ctx.message) mediaType = 'фото';
else if ('video' in ctx.message) mediaType = 'видео';
else if ('document' in ctx.message) mediaType = 'документ';
await ctx.reply(`Получен медиафайл типа: ${mediaType}`);
});
// Обработчик с общей логикой для разных типов
bot.on('message', async (ctx) => {
// Логирование всех входящих сообщений
const userId = ctx.from?.id || 'неизвестный';
const username = ctx.from?.username || 'без имени';
console.log(`Получено сообщение от пользователя ${username} (ID: ${userId})`);
// Здесь можно добавить общую логику для всех типов сообщений
}); |
|
Правильная обработка различных типов сообщений делает бота более функциональным и полезным для пользователей. С помощью grammY можно создать гибкую систему обработчиков, которая будет элегантно и надежно работать с любым контентом, который пользователи могут отправить боту.
Создание интерактивных кнопок и инлайн-клавиатур
Важной особенностью Telegram-ботов является возможность создания интерактивных элементов управления — кнопок и клавиатур, которые значительно улучшают пользовательский опыт и упрощают взаимодействие с ботом. В grammY существует удобный интерфейс для работы с двумя основными типами клавиатур: обычными (reply keyboard) и инлайн-клавиатурами (inline keyboard). Обычные клавиатуры отображаются вместо стандартной клавиатуры ввода и позволяют пользователю выбирать предустановленные варианты ответов. Инлайн-клавиатуры, в свою очередь, отображаются непосредственно под сообщением бота и могут содержать различные типы кнопок с разным функционалом.
Работа с обычными клавиатурами
Обычные клавиатуры удобны, когда нужно предложить пользователю выбор из ограниченного набора вариантов. Их легко создать с помощью класса ReplyKeyboardMarkup:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| import { Keyboard } from 'grammy';
bot.command('menu', async (ctx) => {
// Создаем клавиатуру с кнопками в два ряда
const keyboard = new Keyboard()
.text('Профиль').text('Настройки')
.row()
.text('Помощь').text('Связаться с нами');
// Отправляем сообщение с клавиатурой
await ctx.reply('Выберите действие:', {
reply_markup: keyboard
});
}); |
|
При нажатии на такую кнопку пользователем, бот получает обычное текстовое сообщение, содержащее текст кнопки. Для обработки таких сообщений используются стандартные обработчики:
| TypeScript | 1
2
3
4
5
6
7
8
| bot.hears('Профиль', async (ctx) => {
await ctx.reply('Ваш профиль: ...');
});
bot.hears('Настройки', async (ctx) => {
await ctx.reply('Меню настроек:');
// Дополнительная логика...
}); |
|
При необходимости можно настроить параметры отображения клавиатуры, например, изменить размер кнопок или добавить запрос местоположения или контакта:
| TypeScript | 1
2
3
4
5
6
7
| const keyboard = new Keyboard()
.text('Поделиться контактом', { request_contact: true })
.row()
.text('Отправить местоположение', { request_location: true })
.row()
.text('Обычная кнопка')
.resize(); // Подгоняет размер кнопок под размер текста |
|
Для удаления клавиатуры используется ReplyKeyboardRemove:
| TypeScript | 1
2
3
4
5
| bot.command('close', async (ctx) => {
await ctx.reply('Клавиатура скрыта', {
reply_markup: { remove_keyboard: true }
});
}); |
|
Инлайн-клавиатуры и обработка callback-запросов
Инлайн-клавиатуры позволяют создавать более богатые интерфейсы и не занимают место клавиатуры ввода. Они остаются привязанными к определенному сообщению и могут содержать различные типы кнопок:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| import { InlineKeyboard } from 'grammy';
bot.command('actions', async (ctx) => {
// Создаем инлайн-клавиатуру
const inlineKeyboard = new InlineKeyboard()
.text('Нажми меня', 'button-1').text('Информация', 'info')
.row()
.url('Посетить сайт', 'https://example.com')
.row()
.switchInline('Поделиться', 'share-query');
await ctx.reply('Выберите действие:', {
reply_markup: inlineKeyboard
});
}); |
|
Когда пользователь нажимает на инлайн-кнопку, бот получает специальное обновление типа callback_query. Для обработки таких запросов используется обработчик bot.callbackQuery() или более короткая форма bot.action():
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // Обработка нажатия на кнопку с data: 'button-1'
bot.callbackQuery('button-1', async (ctx) => {
// Отвечаем пользователю всплывающим уведомлением
await ctx.answerCallbackQuery({
text: 'Вы нажали на кнопку!'
});
// Можно также изменить исходное сообщение
await ctx.editMessageText('Кнопка была нажата!');
});
// Можно использовать регулярные выражения для обработки групп кнопок
bot.callbackQuery(/^info-(.+)$/, async (ctx) => {
const parameter = ctx.match[1]; // Извлекаем параметр из callback_data
await ctx.answerCallbackQuery();
await ctx.reply(`Информация о: ${parameter}`);
}); |
|
Метод answerCallbackQuery() обязателен для вызова при обработке callback-запроса, чтобы уведомить клиент Telegram о том, что запрос обработан. Если его не вызвать, пользователь будет видеть индикатор загрузки на кнопке.
Создание динамических меню
Для более сложных сценариев взаимодействия можно создавать динамические многоуровневые меню:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
| function createCategoryMenu(categories: string[]): InlineKeyboard {
const keyboard = new InlineKeyboard();
categories.forEach((category, index) => {
// Добавляем по две кнопки в ряд
if (index % 2 === 0) {
keyboard.text(category, [INLINE]category:${category}[/INLINE]);
} else {
keyboard.text(category, [INLINE]category:${category}[/INLINE]).row();
}
});
// Добавляем кнопку возврата в главное меню
keyboard.row().text('Назад к меню', 'back_to_main');
return keyboard;
}
bot.command('categories', async (ctx) => {
const categories = ['Электроника', 'Одежда', 'Книги', 'Спорт', 'Дом'];
const keyboard = createCategoryMenu(categories);
await ctx.reply('Выберите категорию:', {
reply_markup: keyboard
});
});
// Обработка выбора категории
bot.callbackQuery(/^category:(.+)$/, async (ctx) => {
const category = ctx.match[1];
await ctx.answerCallbackQuery();
// Здесь мы бы загрузили товары из выбранной категории
const products = await getProductsByCategory(category);
// Создаем новую клавиатуру для выбора товаров
const productKeyboard = new InlineKeyboard();
products.forEach(product => {
productKeyboard.text(product.name, [INLINE]product:${product.id}[/INLINE]).row();
});
productKeyboard.text('Назад к категориям', 'back_to_categories');
await ctx.editMessageText(`Товары в категории "${category}":`, {
reply_markup: productKeyboard
});
}); |
|
Такой подход позволяет создавать сложные интерфейсы навигации без необходимости отправлять множество отдельных сообщений.
Типизация callback-данных
Для больших проектов полезно строго типизировать данные callback-запросов. TypeScript позволяет сделать это элегантно:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
| // Определение возможных действий в формате дискриминированных объединений
type CallbackAction =
| { type: 'product'; id: number }
| { type: 'category'; name: string }
| { type: 'page'; number: number };
// Функция для сериализации и десериализации действий
function serializeAction(action: CallbackAction): string {
return JSON.stringify(action);
}
function parseAction(data: string): CallbackAction {
try {
return JSON.parse(data) as CallbackAction;
} catch {
throw new Error(`Невозможно распарсить callback-данные: ${data}`);
}
}
// Создание типизированной кнопки
function createActionButton(text: string, action: CallbackAction): InlineKeyboard {
return new InlineKeyboard().text(text, serializeAction(action));
}
// Обработчик с типизацией
bot.on('callback_query:data', async (ctx) => {
try {
const action = parseAction(ctx.callbackQuery.data);
switch (action.type) {
case 'product':
await handleProduct(ctx, action.id);
break;
case 'category':
await handleCategory(ctx, action.name);
break;
case 'page':
await handlePage(ctx, action.number);
break;
}
} catch (error) {
console.error('Ошибка при обработке callback-запроса:', error);
await ctx.answerCallbackQuery({ text: 'Произошла ошибка' });
}
}); |
|
Создание интерактивных кнопок и клавиатур значительно расширяет возможности взаимодействия пользователя с ботом и делает его использование более интуитивным и удобным. GrammY предоставляет гибкие и мощные инструменты для реализации этих возможностей, а TypeScript добавляет уровень типобезопасности, который помогает избегать ошибок.
Интеграция клавиатур со сценариями диалогов
При создании сложных ботов часто требуется поддерживать состояние диалога и комбинировать клавиатуры с различными сценариями взаимодействия. Для этого удобно использовать менеджер сцен или сессий:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| // Определяем типы состояний для диалога заказа
type OrderState = 'selecting_product' | 'selecting_size' | 'confirming_order' | 'completed';
interface OrderSession {
state: OrderState;
productId?: number;
size?: string;
}
// Создаем клавиатуру в зависимости от состояния диалога
function getKeyboardForState(state: OrderState, data?: any): InlineKeyboard {
const keyboard = new InlineKeyboard();
switch (state) {
case 'selecting_product':
const products = data as Array<{id: number, name: string}>;
products.forEach(product =>
keyboard.text(product.name, [INLINE]product:${product.id}[/INLINE]).row()
);
keyboard.text('Отмена', 'cancel_order');
break;
case 'selecting_size':
['S', 'M', 'L', 'XL'].forEach(size =>
keyboard.text(size, [INLINE]size:${size}[/INLINE])
);
keyboard.row().text('Назад', 'back_to_products').text('Отмена', 'cancel_order');
break;
case 'confirming_order':
keyboard.text('Подтвердить', 'confirm_order').text('Отмена', 'cancel_order');
break;
}
return keyboard;
} |
|
Создание пагинации для списков
Для больших списков элементов эффективно организовать пагинацию с помощью инлайн-клавиатур:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
| function createPaginatedMenu(items: string[], page: number = 0, itemsPerPage: number = 5): InlineKeyboard {
const keyboard = new InlineKeyboard();
const startIndex = page * itemsPerPage;
const endIndex = Math.min(startIndex + itemsPerPage, items.length);
const totalPages = Math.ceil(items.length / itemsPerPage);
// Добавляем элементы текущей страницы
for (let i = startIndex; i < endIndex; i++) {
keyboard.text(items[i], [INLINE]item:${items[i]}[/INLINE]).row();
}
// Добавляем навигационные кнопки
const navRow = [];
if (page > 0) {
navRow.push({ text: '« Назад', callback_data: [INLINE]page:${page - 1}[/INLINE] });
}
navRow.push({ text: [INLINE]${page + 1}/${totalPages}[/INLINE], callback_data: 'current_page' });
if (page < totalPages - 1) {
navRow.push({ text: 'Вперёд »', callback_data: [INLINE]page:${page + 1}[/INLINE] });
}
keyboard.row(...navRow.map(btn => keyboard.text(btn.text, btn.callback_data)));
return keyboard;
}
// Обработчик навигации по страницам
bot.callbackQuery(/^page:(\d+)$/, async (ctx) => {
const page = parseInt(ctx.match[1]);
const items = await getItems(); // Получаем список элементов
const keyboard = createPaginatedMenu(items, page);
await ctx.editMessageText('Список элементов:', {
reply_markup: keyboard
});
await ctx.answerCallbackQuery();
});
// Обработчик для кнопки текущей страницы
bot.callbackQuery('current_page', (ctx) => ctx.answerCallbackQuery({
text: 'Это индикатор текущей страницы',
show_alert: true
})); |
|
Работа с inline_mode и кнопками выбора
Telegram предоставляет возможность использовать ботов в inline-режиме, когда пользователь может вызвать бота из любого чата, написав его имя. Это удобно комбинировать с кнопками для выбора результатов:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| bot.inlineQuery(/.*/, async (ctx) => {
const query = ctx.inlineQuery.query || '';
// Создаем набор результатов для инлайн-режима
const results = [
{
type: 'article',
id: '1',
title: 'Поделиться контактом',
description: 'Отправит ваш контакт в чат',
input_message_content: {
message_text: 'Свяжитесь со мной через @example_user'
},
reply_markup: new InlineKeyboard().url('Написать', 'https://t.me/example_user')
},
{
type: 'article',
id: '2',
title: 'Создать опрос',
description: 'Добавить в чат опрос',
input_message_content: {
message_text: 'Участвуйте в опросе!'
},
reply_markup: new InlineKeyboard()
.text('Голосовать', 'vote_poll')
.row()
.switchInlineCurrent('Создать свой опрос', 'new_poll')
}
];
await ctx.answerInlineQuery(results);
}); |
|
Клавиатуры для специальных функций
Telegram поддерживает специальные типы кнопок для функций оплаты, игр и других возможностей:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // Кнопка оплаты
const paymentKeyboard = new InlineKeyboard()
.pay('Оплатить заказ')
.row()
.text('Отменить', 'cancel_payment');
// Кнопка для запуска игры
const gameKeyboard = new InlineKeyboard()
.game('Играть в тетрис')
.row()
.text('Правила', 'game_rules');
// Кнопка для запроса веб-приложения
const webAppKeyboard = new InlineKeyboard()
.webApp('Открыть приложение', 'https://example.com/webapp'); |
|
Эти расширенные возможности клавиатур делают взаимодействие с ботом более интуитивным и функциональным, предоставляя пользователям привычный интерфейс для решения широкого спектра задач — от навигации по меню до совершения покупок и управления сложными бизнес-процессами.
Обработка медиафайлов и документов
Одна из сильных сторон Telegram-ботов — возможность работать с различными типами медиафайлов и документов. GrammY предоставляет удобные средства для приёма, обработки и отправки различных типов контента. Эта функциональность открывает широкие возможности для создания ботов с богатым пользовательским опытом.
Универсальный подход к обработке медиа
При работе с медиафайлами в grammY используется общий шаблон: получение файла, скачивание содержимого и его обработка. Для всех типов медиа механизм доступа к файлам реализован через метод getFile():
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| bot.on('message:photo', async (ctx) => {
// Получаем объект файла
const file = await ctx.getFile();
// Проверяем успешность получения пути к файлу
if (!file.file_path) {
return ctx.reply('Не удалось получить доступ к файлу');
}
// Формируем URL для загрузки файла
const fileUrl = `https://api.telegram.org/file/bot${env.TELEGRAM_BOT_TOKEN}/${file.file_path}`;
// Дальнейшая обработка...
}); |
|
Обработка изображений с продвинутыми техниками
При работе с изображениями можно реализовать более сложные сценарии, например, анализ содержимого фотографий с помощью нейросетей:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| bot.on('message:photo', async (ctx) => {
// Получаем объект с информацией о фото (берём версию максимального разрешения)
const photoFile = await ctx.getFile();
const photoPath = photoFile.file_path;
if (!photoPath) return ctx.reply('Не удалось обработать изображение');
// Получаем расширение файла для определения MIME-типа
const fileExtMatch = photoPath.match(/\.([^.]+)$/);
const fileExt = fileExtMatch ? fileExtMatch[1] : 'jpeg';
// Скачиваем фото
const photoUrl = `https://api.telegram.org/file/bot${env.TELEGRAM_BOT_TOKEN}/${photoPath}`;
const response = await fetch(photoUrl);
const imageBuffer = await response.arrayBuffer();
// Конвертируем в base64 для отправки в API Gemini
const base64Image = Buffer.from(imageBuffer).toString('base64');
// Формируем запрос к Gemini API
const prompt = [
{
inlineData: {
mimeType: [INLINE]image/${fileExt}[/INLINE],
data: base64Image
}
},
{
text: 'Опиши, что изображено на этой фотографии'
}
];
// Отправляем запрос и получаем ответ
const result = await geminiService.sendMultiModalRequest(prompt);
// Отправляем результат пользователю
return ctx.reply(result);
}); |
|
Интеллектуальная обработка документов
Документы в Telegram могут содержать различные типы файлов от текстовых до архивов. Можно создать гибкий обработчик, который по-разному реагирует на различные типы документов:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| bot.on('message:document', async (ctx) => {
const document = ctx.message.document;
const fileName = document.file_name || 'неизвестный_файл';
const mimeType = document.mime_type || 'application/octet-stream';
// Выводим информацию о полученном документе
await ctx.reply(`Получен документ: ${fileName}\nТип: ${mimeType}`);
// Разные действия в зависимости от типа документа
if (mimeType.startsWith('image/')) {
// Обработка изображений в виде документов
await processImageDocument(ctx);
} else if (mimeType === 'application/pdf') {
// Обработка PDF-файлов
await processPdfDocument(ctx);
} else if (mimeType.startsWith('text/')) {
// Обработка текстовых файлов
await processTextDocument(ctx);
} else if (mimeType.includes('spreadsheet') || mimeType.includes('excel')) {
// Обработка таблиц
await processSpreadsheetDocument(ctx);
} else {
await ctx.reply('Формат этого документа не поддерживается для обработки');
}
}); |
|
Работа с аудио и голосовыми сообщениями
Telegram различает аудиофайлы и голосовые сообщения. Для работы с аудио можно реализовать функции трансформации и анализа:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| bot.on(['message:voice', 'message:audio'], async (ctx) => {
// Определяем тип полученного аудио
const isVoice = 'voice' in ctx.message;
const mediaType = isVoice ? 'голосовое сообщение' : 'аудиофайл';
// Получаем метаданные файла
const fileInfo = isVoice ? ctx.message.voice : ctx.message.audio;
const duration = fileInfo.duration;
await ctx.reply(`Получен ${mediaType} длительностью ${duration} секунд. Обрабатываю...`);
// Получаем доступ к файлу
const file = await ctx.getFile();
const filePath = file.file_path;
if (!filePath) {
return ctx.reply(`Не удалось получить доступ к файлу`);
}
// Дальнейшая обработка...
// Например, преобразование речи в текст или анализ аудио
}); |
|
Отправка медиафайлов пользователю
Бот может не только получать, но и отправлять медиафайлы пользователю. GrammY предоставляет методы для отправки различных типов медиа:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| // Отправка изображения
bot.command('photo', async (ctx) => {
// Можно отправить по URL
await ctx.replyWithPhoto('https://example.com/image.jpg', {
caption: 'Красивое изображение!'
});
// Или из локального файла
await ctx.replyWithPhoto(new InputFile('./images/picture.jpg'));
});
// Отправка документа
bot.command('document', async (ctx) => {
await ctx.replyWithDocument(new InputFile('./files/document.pdf'), {
caption: 'Важный документ'
});
});
// Отправка видео
bot.command('video', async (ctx) => {
await ctx.replyWithVideo(new InputFile('./videos/sample.mp4'), {
caption: 'Интересное видео',
supports_streaming: true
});
}); |
|
Грамотная обработка медиафайлов и документов существенно расширяет возможности бота, позволяя создавать сложные сценарии взаимодействия с пользователем и автоматизировать множество задач, связанных с обменом и анализом контента различных типов.
Разработка системы команд с использованием декораторов в TypeScript
При создании сложных Telegram-ботов с множеством команд возникает потребность в структурированном подходе к их регистрации и управлению. TypeScript предлагает мощный механизм декораторов, который позволяет создавать элегантные декларативные решения для организации командной системы бота. Декораторы — это специальные функции, которые можно применять к классам, методам, свойствам и параметрам для изменения их поведения или добавления метаданных. В контексте разработки Telegram-бота декораторы могут значительно упростить регистрацию команд и их обработчиков.
Прежде всего, для использования декораторов необходимо включить соответствующие настройки в tsconfig.json:
| TypeScript | 1
2
3
4
5
6
7
| {
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
// другие настройки...
}
} |
|
Базовая реализация декоратора команды может выглядеть следующим образом:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
| // Хранилище для регистрации команд
class CommandRegistry {
private static commands: Map<string, Function> = new Map();
static registerCommand(name: string, handler: Function) {
this.commands.set(name, handler);
}
static getCommands(): Map<string, Function> {
return this.commands;
}
}
// Декоратор команды
function Command(name: string) {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
CommandRegistry.registerCommand(name, descriptor.value);
return descriptor;
};
}
// Класс с командами бота
class BotController {
@Command('start')
async handleStart(ctx: Context) {
return ctx.reply('Бот запущен!');
}
@Command('help')
async handleHelp(ctx: Context) {
return ctx.reply('Справка по командам...');
}
}
// Подключение команд к боту
function setupBotCommands(bot: Bot) {
const commands = CommandRegistry.getCommands();
commands.forEach((handler, commandName) => {
bot.command(commandName, (ctx) => handler(ctx));
});
} |
|
Более продвинутая система может учитывать дополнительные параметры команд и включать встроенную валидацию:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| interface CommandOptions {
description: string;
middleware?: Function[];
adminOnly?: boolean;
}
function Command(name: string, options?: CommandOptions) {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
// Создаем обработчик с поддержкой middleware
descriptor.value = async function(ctx: Context) {
// Проверка прав администратора
if (options?.adminOnly && !isAdmin(ctx.from?.id)) {
return ctx.reply('Доступно только администраторам');
}
// Выполнение цепочки middleware
if (options?.middleware) {
for (const middlewareFn of options.middleware) {
const result = await middlewareFn(ctx);
if (result === false) return; // Прерываем выполнение
}
}
// Вызов оригинального метода
return originalMethod.call(this, ctx);
};
// Регистрация команды с метаданными
CommandRegistry.registerCommand(name, descriptor.value, options);
return descriptor;
};
} |
|
Такой подход позволяет также легко реализовать систему параметров команд:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| // Декоратор для параметров команды
function CommandParam(paramName: string, validator?: (value: string) => boolean) {
return function(target: any, propertyKey: string, parameterIndex: number) {
// Сохраняем информацию о параметре для последующей обработки
const existingParams = Reflect.getMetadata('commandParams', target, propertyKey) || [];
existingParams.push({ index: parameterIndex, name: paramName, validator });
Reflect.defineMetadata('commandParams', existingParams, target, propertyKey);
};
}
// Расширенный класс с командами
class EnhancedBotController {
@Command('weather', { description: 'Погода в указанном городе' })
async getWeather(ctx: Context, @CommandParam('city') city: string) {
return ctx.reply(`Погода в городе ${city}: ...`);
}
@Command('remind', { description: 'Установить напоминание' })
async setReminder(
ctx: Context,
@CommandParam('time') time: string,
@CommandParam('text') reminderText: string
) {
// Логика установки напоминания
return ctx.reply(`Напоминание "${reminderText}" установлено на ${time}`);
}
} |
|
Для полноценной работы такой системы необходимо также реализовать механизм извлечения и валидации параметров из сообщений пользователя. Декораторы значительно повышают читаемость кода и упрощают поддержку системы команд бота. Вместо длинных цепочек вызовов bot.command() мы получаем декларативный подход, где каждый метод явно помечен как обработчик определённой команды с указанием всех необходимых метаданных. Этот подход особенно ценен при разработке крупных ботов с десятками команд, организованных в несколько контроллеров. Декораторы позволяют разделить регистрацию команд и их реализацию, делая код более модульным и поддерживаемым.
Продвинутые функции
После освоения базовых принципов создания Telegram-бота на TypeScript с использованием grammY, пора погрузиться в более сложные аспекты разработки, которые сделают вашего бота по-настоящему функциональным и гибким. Продвинутые возможности библиотеки позволяют реализовать сложные сценарии взаимодействия и более глубокую интеграцию с внешними сервисами.
GrammY предоставляет несколько мощных инструментов, выходящих за рамки простой обработки команд и сообщений. К ним относятся система middleware, работа с пользовательскими состояниями, расширенные возможности для интеграции с внешними API и многое другое. Эти функции формируют надежный фундамент для построения сложных ботов с богатой функциональностью. Одним из ключевых элементов архитектуры grammY является система middleware — промежуточных обработчиков, которые выполняются в определенной последовательности при получении обновления от Telegram. Эта система позволяет разделить логику обработки запросов на отдельные модули, что делает код более чистым и поддерживаемым.
Работа с состояниями пользователей — еще одна важная возможность, позволяющая создавать сложные диалоговые сценарии. Бот может "запоминать" контекст разговора с каждым пользователем и реагировать соответственно, что делает взаимодействие более естественным и интуитивным. Интеграция с внешними API расширяет возможности бота за пределы платформы Telegram. Будь то погодный сервис, база данных или AI-платформа, возможность подключения к другим сервисам превращает Telegram-бота в мощный интерфейс для взаимодействия с различными системами. Локализация и мультиязычность становятся все более важными аспектами при разработке ботов с широкой аудиторией. GrammY предоставляет инструменты для создания ботов, способных общаться с пользователями на их родном языке, что значительно улучшает пользовательский опыт.
Реализация сложных диалоговых сценариев с помощью сессий и сцен — еще одна продвинутая техника, которая позволяет создавать многошаговые взаимодействия, такие как формы регистрации, опросы или процессы заказа.
Наконец, работа с базами данных для хранения пользовательской информации делает ботов по-настоящему персонализированными и способными предоставлять контекстно-зависимые ответы на основе предыдущих взаимодействий или сохраненных предпочтений.
Эти продвинутые функции расширяют горизонты возможностей ваших Telegram-ботов, превращая их из простых обработчиков команд в полноценные приложения с богатым функционалом и интуитивным пользовательским опытом. В следующих разделах мы подробно рассмотрим каждую из этих возможностей.
Работа с middleware
Middleware (промежуточное программное обеспечение) — один из самых мощных механизмов в grammY, который позволяет структурировать обработку сообщений и создавать модульную архитектуру бота. По сути, middleware представляет собой функцию, которая получает контекст сообщения и может выполнить определённые действия до, во время или после обработки запроса.
Архитектура middleware в grammY основана на принципе "цепочки ответственности", где каждый middleware-обработчик может либо завершить обработку запроса, либо передать управление следующему в цепочке с помощью функции next():
| TypeScript | 1
2
3
4
5
6
7
8
9
10
| bot.use(async (ctx, next) => {
console.log('Middleware 1: До следующего обработчика');
await next(); // Передаем управление следующему middleware
console.log('Middleware 1: После следующего обработчика');
});
bot.use(async (ctx, next) => {
console.log('Middleware 2: Выполняется');
await next();
}); |
|
Когда бот получает сообщение, middleware-функции вызываются в том порядке, в котором они были добавлены через метод bot.use(). Это создаёт структуру напоминающую луковицу — каждый слой может обрабатывать запрос до и после всех внутренних слоёв.
Практические применения middleware
Middleware в grammY можно использовать для решения множества задач:
1. Логирование и измерение производительности:
| TypeScript | 1
2
3
4
5
6
7
8
9
| bot.use(async (ctx, next) => {
const startTime = Date.now();
console.log(`Обработка сообщения от ${ctx.from?.username || 'неизвестного пользователя'}`);
await next();
const ms = Date.now() - startTime;
console.log(`Обработка заняла ${ms}ms`);
}); |
|
2. Авторизация и проверка прав:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| const adminMiddleware = async (ctx, next) => {
const userId = ctx.from?.id;
if (!userId || !isAdmin(userId)) {
return ctx.reply('У вас нет прав для выполнения этого действия');
}
await next(); // Если администратор, продолжаем обработку
};
// Применение к конкретной команде
bot.command('admin', adminMiddleware, (ctx) => {
return ctx.reply('Вы вошли в панель администратора');
}); |
|
3. Обработка исключений:
| TypeScript | 1
2
3
4
5
6
7
8
| bot.use(async (ctx, next) => {
try {
await next();
} catch (error) {
console.error('Произошла ошибка:', error);
await ctx.reply('Извините, произошла ошибка. Попробуйте позже.');
}
}); |
|
4. Загрузка данных пользователя:
| TypeScript | 1
2
3
4
5
6
7
| bot.use(async (ctx, next) => {
if (ctx.from) {
ctx.dbUser = await getUserFromDatabase(ctx.from.id);
}
await next();
}); |
|
Композиция middleware-функций
Одно из преимуществ middleware — возможность создавать переиспользуемые компоненты и объединять их в более сложные обработчики:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| // Создание переиспользуемых middleware
function throttle(limit: number, interval: number) {
const users = new Map<number, number[]>();
return async (ctx: Context, next: NextFunction) => {
const userId = ctx.from?.id;
if (!userId) return next();
const now = Date.now();
const userRequests = users.get(userId) || [];
// Удаляем устаревшие запросы
const recentRequests = userRequests.filter(time => now - time < interval);
if (recentRequests.length >= limit) {
return ctx.reply('Пожалуйста, не отправляйте сообщения так часто');
}
recentRequests.push(now);
users.set(userId, recentRequests);
return next();
};
}
// Использование
bot.use(throttle(5, 10000)); // Не более 5 запросов за 10 секунд |
|
Middleware может быть глобальным (применяться ко всем сообщениям) или локальным (применяться только к определённым командам или типам сообщений). Это даёт гибкость в организации логики бота:
| TypeScript | 1
2
3
4
5
6
7
| // Глобальный middleware
bot.use(loggerMiddleware);
// Локальный middleware для конкретной команды
bot.command('settings', authMiddleware, (ctx) => {
return ctx.reply('Настройки вашего профиля');
}); |
|
Использование middleware позволяет создавать чистую, модульную и легко тестируемую архитектуру бота, где каждая функция имеет конкретную ответственность. Это значительно упрощает поддержку и развитие сложных ботов с разнообразной функциональностью.
Хранение состояний пользователей
Разработка интерактивных Telegram-ботов часто требует сохранения информации о пользователях между сообщениями. Без механизма хранения состояний бот воспринимал бы каждое сообщение как изолированное событие, не имея представления о предыдущих взаимодействиях. Это значительно ограничивало бы создание сложных сценариев взаимодействия, таких как многоступенчатые диалоги или персонализированные ответы. GrammY предоставляет элегантное решение этой задачи через систему сессий. Сессии позволяют сохранять данные для каждого пользователя между различными обновлениями и обращаться к ним в процессе работы бота. Для реализации этой возможности используется плагин @grammyjs/storage-* в сочетании с middleware для сессий. Базовая настройка сессий выглядит следующим образом:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| import { Bot, Context, session, SessionFlavor } from 'grammy';
// Определяем структуру данных сессии
interface SessionData {
step: 'idle' | 'waiting_name' | 'waiting_email';
userData: {
name?: string;
email?: string;
};
counter: number;
}
// Расширяем тип контекста, добавляя сессию
type MyContext = Context & SessionFlavor<SessionData>;
// Создаем бота с указанным типом контекста
const bot = new Bot<MyContext>(process.env.TELEGRAM_BOT_TOKEN!);
// Настраиваем middleware для сессий
bot.use(session({
initial: (): SessionData => ({
step: 'idle',
userData: {},
counter: 0
})
})); |
|
По умолчанию grammY использует хранилище в памяти, что подходит для разработки и небольших ботов. Однако для продакшн-среды рекомендуется использовать более надёжные хранилища:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
| import { FileAdapter } from '@grammyjs/storage-file';
// Хранение сессий в файловой системе
const fileAdapter = new FileAdapter<SessionData>({
dirName: 'sessions' // Директория для хранения файлов сессий
});
bot.use(session({
initial: () => ({ step: 'idle', userData: {}, counter: 0 }),
storage: fileAdapter
})); |
|
Для масштабируемых решений можно использовать хранилища на основе баз данных. GrammY поддерживает интеграции с популярными базами данных через соответствующие адаптеры:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
| import { MongoDBAdapter } from '@grammyjs/storage-mongodb';
// Настройка MongoDB адаптера
const mongoAdapter = new MongoDBAdapter<SessionData>({
collection: db.collection('sessions')
});
// Подключение к боту
bot.use(session({
initial: () => ({ step: 'idle', userData: {}, counter: 0 }),
storage: mongoAdapter
})); |
|
После настройки хранилища, доступ к данным сессии осуществляется через свойство ctx.session:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| // Обработчик команды /start
bot.command('start', async (ctx) => {
// Устанавливаем начальный шаг диалога
ctx.session.step = 'waiting_name';
await ctx.reply('Пожалуйста, введите ваше имя:');
});
// Обработка текстовых сообщений с учетом состояния диалога
bot.on('message:text', async (ctx) => {
switch (ctx.session.step) {
case 'waiting_name':
ctx.session.userData.name = ctx.message.text;
ctx.session.step = 'waiting_email';
await ctx.reply(`Приятно познакомиться, ${ctx.session.userData.name}! Теперь введите ваш email:`);
break;
case 'waiting_email':
ctx.session.userData.email = ctx.message.text;
ctx.session.step = 'idle';
await ctx.reply(`Спасибо! Ваши данные:
Имя: ${ctx.session.userData.name}
Email: ${ctx.session.userData.email}`);
break;
case 'idle':
// Увеличиваем счетчик сообщений
ctx.session.counter++;
await ctx.reply(`Вы отправили ${ctx.session.counter} сообщений`);
break;
}
}); |
|
Для хранения временных данных в рамках одного запроса можно использовать объект ctx.state, который существует только во время обработки текущего обновления и не сохраняется между запросами:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
| bot.use((ctx, next) => {
// Сохраняем время начала обработки
ctx.state.startTime = Date.now();
return next();
});
bot.on('message', (ctx) => {
const processingTime = Date.now() - ctx.state.startTime;
return ctx.reply(`Обработка вашего сообщения заняла ${processingTime}ms`);
}); |
|
Грамотное использование механизма сессий и состояний пользователя позволяет разрабатывать сложные, интерактивные боты с естественным пользовательским опытом и персонализированными взаимодействиями.
Интеграция с внешними API
Создание действительно полезного и функционального Telegram-бота редко ограничивается простой обработкой сообщений. Настоящая сила ботов раскрывается при интеграции с внешними API, что позволяет расширить их возможности далеко за пределы платформы Telegram. В данном разделе мы рассмотрим, как правильно интегрировать внешние сервисы с ботом на grammY. Интеграция с внешними API обычно требует создания специализированных сервисных классов, которые инкапсулируют всю логику взаимодействия с API. Такой подход обеспечивает чистую архитектуру и упрощает поддержку кода:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| // services/weather-service.ts
export class WeatherService {
private readonly apiKey: string;
private readonly baseUrl: string = 'https://api.weatherapi.com/v1';
constructor(apiKey: string) {
this.apiKey = apiKey;
}
async getWeather(city: string): Promise<WeatherData> {
const url = `${this.baseUrl}/current.json?key=${this.apiKey}&q=${encodeURIComponent(city)}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Ошибка запроса: ${response.status}`);
}
return await response.json();
}
} |
|
При работе с внешними API важно правильно типизировать получаемые данные, чтобы использовать все преимущества TypeScript:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| // types/weather.ts
export interface WeatherData {
location: {
name: string;
region: string;
country: string;
lat: number;
lon: number;
localtime: string;
};
current: {
temp_c: number;
condition: {
text: string;
icon: string;
};
humidity: number;
wind_kph: number;
};
} |
|
Подключение сервиса к боту осуществляется через контекст или напрямую через обработчики:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| // Создание экземпляра сервиса
const weatherService = new WeatherService(env.WEATHER_API_KEY);
// Обработчик команды погоды
bot.command('weather', async (ctx) => {
const city = ctx.match;
if (!city) {
return ctx.reply('Укажите город после команды. Например: /weather Москва');
}
try {
// Отправка индикатора набора текста, пока выполняется запрос
await ctx.api.sendChatAction(ctx.chat.id, 'typing');
const weatherData = await weatherService.getWeather(city);
// Форматируем ответ с данными о погоде
const response = `Погода в ${weatherData.location.name}, ${weatherData.location.country}:
Температура: ${weatherData.current.temp_c}°C
Условия: ${weatherData.current.condition.text}
Влажность: ${weatherData.current.humidity}%
Ветер: ${weatherData.current.wind_kph} км/ч`;
await ctx.reply(response);
} catch (error) {
console.error('Ошибка получения данных о погоде:', error);
await ctx.reply('Не удалось получить данные о погоде. Попробуйте позже или проверьте название города.');
}
}); |
|
Для более сложных API, требующих аутентификации, полезно создать отдельный класс для работы с HTTP-запросами:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
| // services/api-client.ts
export class ApiClient {
private readonly baseUrl: string;
private readonly headers: Record<string, string>;
constructor(baseUrl: string, apiKey: string) {
this.baseUrl = baseUrl;
this.headers = {
'Authorization': [INLINE]Bearer ${apiKey}[/INLINE],
'Content-Type': 'application/json'
};
}
async get<T>(path: string, params?: Record<string, string>): Promise<T> {
const url = new URL(path, this.baseUrl);
if (params) {
Object.entries(params).forEach(([key, value]) => {
url.searchParams.append(key, value);
});
}
const response = await fetch(url.toString(), {
method: 'GET',
headers: this.headers
});
if (!response.ok) {
throw new Error(`API запрос завершился с ошибкой: ${response.status}`);
}
return await response.json();
}
async post<T, U>(path: string, data: T): Promise<U> {
const url = new URL(path, this.baseUrl).toString();
const response = await fetch(url, {
method: 'POST',
headers: this.headers,
body: JSON.stringify(data)
});
if (!response.ok) {
throw new Error(`API запрос завершился с ошибкой: ${response.status}`);
}
return await response.json();
}
} |
|
При интеграции с популярными API часто существуют готовые клиенты, которые упрощают взаимодействие:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| // services/openai-service.ts
import { Configuration, OpenAIApi } from 'openai';
export class OpenAIService {
private openai: OpenAIApi;
constructor(apiKey: string) {
const configuration = new Configuration({ apiKey });
this.openai = new OpenAIApi(configuration);
}
async generateText(prompt: string): Promise<string> {
try {
const response = await this.openai.createCompletion({
model: 'text-davinci-003',
prompt,
max_tokens: 500,
temperature: 0.7
});
return response.data.choices[0].text?.trim() || 'Нет ответа';
} catch (error) {
console.error('Ошибка при запросе к OpenAI:', error);
throw new Error('Не удалось сгенерировать текст');
}
}
} |
|
При интеграции нескольких API важно обеспечить устойчивость бота к ошибкам и задержкам. Для этого рекомендуется использовать таймауты, повторные попытки и резервные механизмы:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| async function fetchWithRetry<T>(
fetchFn: () => Promise<T>,
retries: number = 3,
timeout: number = 5000
): Promise<T> {
let attempts = 0;
while (attempts < retries) {
try {
// Создаем промис с таймаутом
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
const result = await Promise.race([
fetchFn(),
new Promise<never>((_, reject) => {
controller.signal.addEventListener('abort', () => {
reject(new Error('Запрос отменен по таймауту'));
});
})
]);
clearTimeout(timeoutId);
return result;
} catch (error) {
attempts++;
if (attempts >= retries) throw error;
// Экспоненциальная задержка между попытками
await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, attempts - 1)));
}
}
throw new Error('Все попытки запроса завершились неудачей');
} |
|
Реализация локализации для многоязычных ботов
Поддержка нескольких языков значительно расширяет аудиторию Telegram-бота и улучшает пользовательский опыт. Локализация позволяет автоматически предоставлять контент на предпочитаемом языке пользователя, делая взаимодействие с ботом более естественным. GrammY предлагает удобный механизм для реализации многоязычной поддержки через специализированный плагин @grammyjs/i18n. Для начала установим необходимые зависимости:
| Bash | 1
| npm install @grammyjs/i18n |
|
Плагин @grammyjs/i18n базируется на популярной библиотеке i18n для Node.js, но адаптирован специально для работы с grammY. Он предоставляет простой интерфейс для управления переводами и автоматического определения языка пользователя.
Организация файлов локализации обычно выглядит следующим образом:
| TypeScript | 1
2
3
4
5
| src/
└── locales/
├── ru.json
├── en.json
└── es.json |
|
Каждый файл содержит переводы для конкретного языка в формате JSON:
| JSON | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| // locales/ru.json
{
"welcome": "Добро пожаловать, {{name}}!",
"help": "Список доступных команд:\n/start - Начать работу\n/settings - Настройки\n/help - Помощь",
"language_changed": "Язык изменен на русский!",
"commands": {
"not_understood": "Извините, я не понял эту команду."
},
"errors": {
"general": "Произошла ошибка. Пожалуйста, попробуйте позже."
}
}
// locales/en.json
{
"welcome": "Welcome, {{name}}!",
"help": "Available commands:\n/start - Start bot\n/settings - Settings\n/help - Help",
"language_changed": "Language has been changed to English!",
"commands": {
"not_understood": "Sorry, I didn't understand that command."
},
"errors": {
"general": "An error occurred. Please try again later."
}
} |
|
Настройка плагина i18n в grammY выполняется следующим образом:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| import { Bot, Context } from 'grammy';
import { I18n, I18nFlavor } from '@grammyjs/i18n';
// Определяем тип контекста с поддержкой i18n
type MyContext = Context & I18nFlavor;
// Создаем экземпляр бота с нужным типом контекста
const bot = new Bot<MyContext>(process.env.TELEGRAM_BOT_TOKEN!);
// Инициализируем плагин i18n
const i18n = new I18n({
defaultLocale: 'ru', // Язык по умолчанию
directory: 'src/locales', // Директория с файлами переводов
useSession: true, // Использовать сессии для хранения выбранного языка
templateData: {
pluralize: (count, forms) => {
// Функция для плюрализации в русском языке
return count % 10 === 1 && count % 100 !== 11
? forms[0] // одна книга
: count % 10 >= 2 && count % 10 <= 4 && (count % 100 < 10 || count % 100 >= 20)
? forms[1] // две/три/четыре книги
: forms[2]; // пять книг
}
}
});
// Подключаем middleware для i18n
bot.use(i18n.middleware()); |
|
Для определения языка пользователя существует несколько стратегий:
1. Использование языка, указанного в настройках клиента Telegram:
| TypeScript | 1
2
3
4
5
6
7
8
| bot.use((ctx, next) => {
// Определяем язык из языковых настроек пользователя
const userLanguage = ctx.from?.language_code;
if (userLanguage && i18n.locales.includes(userLanguage)) {
ctx.i18n.locale(userLanguage);
}
return next();
}); |
|
2. Предоставление пользователю возможности выбрать язык:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| bot.command('language', async (ctx) => {
const keyboard = new InlineKeyboard()
.text('Русский', 'lang:ru')
.text('English', 'lang:en')
.row()
.text('Español', 'lang:es');
await ctx.reply('Выберите язык / Choose language / Elija el idioma:', {
reply_markup: keyboard
});
});
bot.callbackQuery(/^lang:(.+)$/, async (ctx) => {
const lang = ctx.match[1];
if (i18n.locales.includes(lang)) {
// Устанавливаем выбранный язык в сессии
ctx.i18n.locale(lang);
await ctx.reply(ctx.i18n.t('language_changed'));
}
await ctx.answerCallbackQuery();
}); |
|
Использование переводов в коде бота просто и интуитивно благодаря методу t():
| TypeScript | 1
2
3
4
5
6
7
8
9
10
| bot.command('start', async (ctx) => {
const name = ctx.from?.first_name || 'пользователь';
// Используем перевод с параметром
await ctx.reply(ctx.i18n.t('welcome', { name }));
});
bot.command('help', async (ctx) => {
// Используем простой перевод
await ctx.reply(ctx.i18n.t('help'));
}); |
|
Для более сложных случаев, таких как множественные формы или условное форматирование, можно использовать расширенные возможности плагина:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
| // В файле локализации
// ru.json
{
"items_count": "{{count}} {{pluralize(count, ['элемент', 'элемента', 'элементов'])}}"
}
// Использование в коде
bot.command('items', async (ctx) => {
const itemsCount = 5;
await ctx.reply(ctx.i18n.t('items_count', { count: itemsCount }));
// Выведет: "5 элементов"
}); |
|
Тщательно продуманная система локализации делает бота доступным для международной аудитории и значительно улучшает пользовательский опыт, позволяя каждому общаться с ботом на родном языке.
Реализация сценариев диалогов с пользователем (sessions & scenes)
Создание естественного диалога между ботом и пользователем — одна из наиболее сложных задач при разработке Telegram-ботов. Простая обработка команд и сообщений недостаточна для сложных взаимодействий, таких как регистрация, заполнение форм или прохождение многоступенчатых процессов. Здесь на помощь приходят сценарии диалогов, реализуемые через сессии и сцены.
В экосистеме grammY для работы со сложными диалогами существует специальный плагин @grammyjs/conversations, который предоставляет интуитивный интерфейс для создания многошаговых взаимодействий:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
| import { Bot } from 'grammy';
import { conversations, createConversation } from '@grammyjs/conversations';
// Установка плагина
const bot = new Bot(process.env.TELEGRAM_BOT_TOKEN!);
bot.use(conversations());
// Определение диалога регистрации
async function registrationConversation(conversation, ctx) {
// Шаг 1: Запрос имени
await ctx.reply('Пожалуйста, введите ваше имя:');
const { message: nameMessage } = await conversation.wait();
const name = nameMessage.text;
// Шаг 2: Запрос email
await ctx.reply(`Спасибо, ${name}! Теперь введите ваш email:`);
const { message: emailMessage } = await conversation.wait();
const email = emailMessage.text;
// Шаг 3: Запрос возраста с валидацией
await ctx.reply('Укажите ваш возраст:');
let age: number | null = null;
while (age === null) {
const { message: ageMessage } = await conversation.wait();
const ageText = ageMessage.text;
const parsedAge = parseInt(ageText || '');
if (isNaN(parsedAge) || parsedAge < 12 || parsedAge > 120) {
await ctx.reply('Пожалуйста, введите корректный возраст (от 12 до 120):');
} else {
age = parsedAge;
}
}
// Завершение регистрации
await ctx.reply(`Регистрация завершена!
Имя: ${name}
Email: ${email}
Возраст: ${age}`);
// Возвращаем собранные данные
return { name, email, age };
}
// Регистрируем диалог
bot.use(createConversation(registrationConversation));
// Команда для запуска диалога
bot.command('register', async (ctx) => {
await ctx.conversation.enter('registrationConversation');
}); |
|
Главное преимущество такого подхода — линейная структура кода, которая точно отражает порядок шагов в диалоге. Нет необходимости управлять состояниями вручную или писать сложные условные конструкции — плагин conversations автоматически сохраняет контекст диалога и обеспечивает его продолжение при получении новых сообщений.
Для более сложных сценариев можно использовать условную логику и ветвление диалогов:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
| async function orderConversation(conversation, ctx) {
// Начало заказа
await ctx.reply('Что вы хотите заказать?', {
reply_markup: { keyboard: [['Пицца', 'Суши'], ['Отмена']] }
});
const { message: foodMessage } = await conversation.wait();
const foodChoice = foodMessage.text;
if (foodChoice === 'Отмена') {
await ctx.reply('Заказ отменен', {
reply_markup: { remove_keyboard: true }
});
return null;
}
// Запрос размера, если выбрана пицца
let size = '';
if (foodChoice === 'Пицца') {
await ctx.reply('Выберите размер:', {
reply_markup: { keyboard: [['Маленькая', 'Средняя', 'Большая']] }
});
const { message: sizeMessage } = await conversation.wait();
size = sizeMessage.text || '';
}
// Запрос адреса доставки
await ctx.reply('Укажите адрес доставки:', {
reply_markup: { remove_keyboard: true }
});
const { message: addressMessage } = await conversation.wait();
const address = addressMessage.text;
// Подтверждение заказа
let orderDetails = `Ваш заказ:
Блюдо: ${foodChoice}`;
if (size) {
orderDetails += `
Размер: ${size}`;
}
orderDetails += `
Адрес доставки: ${address}`;
await ctx.reply(`${orderDetails}
Подтверждаете заказ?`, {
reply_markup: { keyboard: [['Да', 'Нет']] }
});
const { message: confirmMessage } = await conversation.wait();
const confirmation = confirmMessage.text;
if (confirmation === 'Да') {
await ctx.reply('Заказ принят! Ожидайте доставку.', {
reply_markup: { remove_keyboard: true }
});
return { food: foodChoice, size, address };
} else {
await ctx.reply('Заказ отменен', {
reply_markup: { remove_keyboard: true }
});
return null;
}
} |
|
Помимо линейных диалогов, grammY позволяет создавать модульные сцены, которые можно комбинировать между собой. Для более структурированного подхода можно организовать сцены в отдельные файлы:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| // scenes/profile-scene.ts
export function createProfileScene() {
return async function profileScene(conversation, ctx) {
// Логика сцены редактирования профиля
};
}
// scenes/order-scene.ts
export function createOrderScene() {
return async function orderScene(conversation, ctx) {
// Логика сцены оформления заказа
};
}
// Регистрация сцен в основном файле
import { createProfileScene } from './scenes/profile-scene';
import { createOrderScene } from './scenes/order-scene';
bot.use(createConversation(createProfileScene()));
bot.use(createConversation(createOrderScene())); |
|
Для работы с разветвленными сценариями можно использовать вложенные диалоги:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| async function mainConversation(conversation, ctx) {
await ctx.reply('Выберите действие:', {
reply_markup: { keyboard: [['Профиль', 'Заказ'], ['Выход']] }
});
const { message } = await conversation.wait();
const choice = message.text;
if (choice === 'Профиль') {
// Переход к вложенному диалогу
return await conversation.external(() => profileConversation(conversation, ctx));
} else if (choice === 'Заказ') {
return await conversation.external(() => orderConversation(conversation, ctx));
} else {
await ctx.reply('До свидания!', {
reply_markup: { remove_keyboard: true }
});
return null;
}
} |
|
Иногда в процессе диалога возникает необходимость обработать ситуации, когда пользователь не отвечает в течение определенного времени или решает прервать беседу. grammY позволяет элегантно решить эту проблему, добавив таймауты и обработчики прерывания:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| async function feedbackConversation(conversation, ctx) {
// Устанавливаем таймаут на диалог
conversation.setTimeout(300000); // 5 минут
try {
await ctx.reply('Пожалуйста, оцените наш сервис от 1 до 5:');
// Ожидаем рейтинг с таймаутом
const { message: ratingMsg } = await conversation.wait();
const rating = parseInt(ratingMsg.text || '0');
if (rating < 1 || rating > 5 || isNaN(rating)) {
await ctx.reply('Пожалуйста, введите число от 1 до 5.');
return await conversation.restart();
}
await ctx.reply('Оставьте, пожалуйста, комментарий к вашей оценке:');
const { message: commentMsg } = await conversation.wait();
const comment = commentMsg.text || '';
await ctx.reply(`Спасибо за ваш отзыв!
Оценка: ${rating}/5
Комментарий: ${comment}`);
// Сохраняем отзыв в базу данных
return { rating, comment };
} catch (error) {
if (error.message === 'CONVERSATION_TIMEOUT') {
await ctx.reply('Время ожидания истекло. Для оставления отзыва начните сначала.');
} else {
throw error;
}
}
} |
|
Важным аспектом является контроль жизненного цикла диалога. Иногда требуется предоставить пользователю возможность отменить текущую операцию или вернуться на предыдущий шаг:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
| async function surveyConversation(conversation, ctx) {
// Функция для обработки команды отмены
conversation.beforeGlobalCommand('cancel', async (ctx) => {
await ctx.reply('Опрос отменен', {
reply_markup: { remove_keyboard: true }
});
return true; // Прерываем диалог
});
let answers = {
age: null,
gender: null,
occupation: null
};
// Начало опроса
await ctx.reply('Добро пожаловать в опрос! Для отмены в любой момент введите /cancel');
// Вопрос о возрасте
await ctx.reply('Сколько вам лет?');
const ageResponse = await conversation.waitFor(':text');
answers.age = ageResponse.msg.text;
// Вопрос о поле
await ctx.reply('Укажите ваш пол:', {
reply_markup: {
keyboard: [['Мужской', 'Женский'], ['Предпочитаю не указывать']]
}
});
const genderResponse = await conversation.waitFor(':text');
answers.gender = genderResponse.msg.text;
// Вопрос о роде занятий
await ctx.reply('Чем вы занимаетесь?', {
reply_markup: { remove_keyboard: true }
});
const occupationResponse = await conversation.waitFor(':text');
answers.occupation = occupationResponse.msg.text;
// Завершение опроса
await ctx.reply(`Спасибо за участие в опросе! Ваши ответы:
Возраст: ${answers.age}
Пол: ${answers.gender}
Род занятий: ${answers.occupation}`);
return answers;
} |
|
Для более сложных взаимодействий полезно использовать метод waitFor(), который позволяет ожидать сообщения определенного типа или соответствующие заданному условию:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| // Ожидание сообщения с геолокацией
await ctx.reply('Отправьте вашу локацию для поиска ближайших ресторанов');
const locationResponse = await conversation.waitFor('message:location');
const location = locationResponse.msg.location;
// Ожидание фотографии
await ctx.reply('Загрузите фотографию блюда');
const photoResponse = await conversation.waitFor('message:photo');
const photo = photoResponse.msg.photo;
// Ожидание сообщения, соответствующего условию
await ctx.reply('Введите корректный email');
const emailResponse = await conversation.waitFor((ctx) => {
const text = ctx.message?.text || '';
return /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(text);
}, {
otherwise: (ctx) => ctx.reply('Пожалуйста, введите корректный email-адрес')
});
const email = emailResponse.msg.text; |
|
Грамотная организация сценариев диалогов делает взаимодействие с ботом естественным и удобным для пользователя. В сочетании с хорошо продуманным интерфейсом, правильной обработкой ошибок и четкими инструкциями, диалоговые сцены могут сделать опыт использования бота максимально близким к общению с реальным человеком, что является конечной целью большинства проектов по разработке чат-ботов.
Работа с базами данных для хранения пользовательских данных
Эффективное хранение и управление пользовательскими данными — важнейший компонент любого серьезного Telegram-бота. По мере роста пользовательской базы простые решения с хранением информации в памяти или файлах становятся недостаточными, и возникает необходимость перехода на полноценные базы данных. Правильный выбор и настройка базы данных обеспечивают надежность, масштабируемость и безопасность хранения пользовательской информации.
GrammY предоставляет гибкие возможности для интеграции с различными системами управления базами данных. В зависимости от масштаба проекта, типа данных и требований к производительности, можно выбрать оптимальное решение — от легковесных встраиваемых баз данных до мощных распределенных систем.
Интеграция с MongoDB
MongoDB — популярный выбор для Telegram-ботов благодаря гибкой схеме данных и удобной работе с JSON-подобными документами. Интеграция MongoDB с ботом на grammY начинается с установки необходимых пакетов:
| Bash | 1
| npm install mongodb mongoose |
|
Базовая настройка подключения к MongoDB с помощью Mongoose выглядит следующим образом:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| import mongoose from 'mongoose';
import { config } from './config';
// Подключение к базе данных
export async function connectToDatabase() {
try {
await mongoose.connect(config.databaseUrl);
console.log('Успешное подключение к MongoDB');
} catch (error) {
console.error('Ошибка подключения к MongoDB:', error);
process.exit(1);
}
}
// Определение схемы пользователя
const userSchema = new mongoose.Schema({
telegramId: { type: Number, required: true, unique: true },
username: { type: String },
firstName: { type: String, required: true },
lastName: { type: String },
registeredAt: { type: Date, default: Date.now },
settings: {
language: { type: String, default: 'ru' },
notifications: { type: Boolean, default: true }
},
lastInteraction: { type: Date },
statistics: {
commandsUsed: { type: Number, default: 0 },
messagesReceived: { type: Number, default: 0 }
}
});
// Создание модели
export const User = mongoose.model('User', userSchema); |
|
Для работы с пользовательскими данными в контексте бота удобно создать сервисный класс:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
| export class UserService {
async findOrCreateUser(telegramUser: any): Promise<typeof User> {
if (!telegramUser) throw new Error('Данные пользователя не предоставлены');
// Поиск существующего пользователя
const existingUser = await User.findOne({ telegramId: telegramUser.id });
if (existingUser) {
// Обновляем дату последнего взаимодействия
existingUser.lastInteraction = new Date();
await existingUser.save();
return existingUser;
}
// Создание нового пользователя
const newUser = new User({
telegramId: telegramUser.id,
username: telegramUser.username,
firstName: telegramUser.first_name,
lastName: telegramUser.last_name,
registeredAt: new Date(),
lastInteraction: new Date()
});
await newUser.save();
return newUser;
}
async updateUserLanguage(telegramId: number, language: string): Promise<void> {
await User.updateOne(
{ telegramId },
{ 'settings.language': language }
);
}
async incrementStatistics(telegramId: number, field: 'commandsUsed' | 'messagesReceived'): Promise<void> {
const update: any = {};
update[`statistics.${field}`] = 1;
await User.updateOne(
{ telegramId },
{ $inc: update }
);
}
} |
|
Подключение сервиса к боту через middleware позволяет автоматически загружать информацию о пользователе при каждом взаимодействии:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| const userService = new UserService();
// Middleware для работы с пользователями
bot.use(async (ctx, next) => {
if (ctx.from) {
try {
// Загружаем или создаем пользователя
ctx.dbUser = await userService.findOrCreateUser(ctx.from);
// Увеличиваем статистику
if (ctx.message?.text?.startsWith('/')) {
await userService.incrementStatistics(ctx.from.id, 'commandsUsed');
} else {
await userService.incrementStatistics(ctx.from.id, 'messagesReceived');
}
} catch (error) {
console.error('Ошибка при работе с пользователем:', error);
}
}
await next();
}); |
|
Работа с SQL базами данных
Для проектов, требующих строгой структуры данных и сложных связей между таблицами, SQL базы данных могут быть более подходящим выбором. TypeORM — популярный ORM для TypeScript, который упрощает работу с SQL базами данных:
| Bash | 1
| npm install typeorm pg reflect-metadata |
|
Базовая настройка TypeORM для работы с PostgreSQL:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
| import { createConnection, Connection, Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
import { config } from './config';
// Определение сущности пользователя
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
telegramId: number;
@Column({ nullable: true })
username: string;
@Column()
firstName: string;
@Column({ nullable: true })
lastName: string;
@Column()
registeredAt: Date;
@Column({ default: 'ru' })
language: string;
@Column({ default: true })
notifications: boolean;
}
// Инициализация соединения с базой данных
export async function initializeDatabase(): Promise<Connection> {
return createConnection({
type: 'postgres',
host: config.dbHost,
port: config.dbPort,
username: config.dbUser,
password: config.dbPassword,
database: config.dbName,
entities: [User],
synchronize: process.env.NODE_ENV !== 'production', // В production лучше использовать миграции
logging: process.env.NODE_ENV !== 'production'
});
} |
|
Репозиторий для работы с пользователями при использовании TypeORM:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| export class UserRepository {
constructor(private connection: Connection) {}
async findOrCreateUser(telegramUser: any): Promise<User> {
const userRepository = this.connection.getRepository(User);
// Поиск пользователя
let user = await userRepository.findOne({ where: { telegramId: telegramUser.id } });
if (!user) {
// Создание нового пользователя
user = new User();
user.telegramId = telegramUser.id;
user.username = telegramUser.username;
user.firstName = telegramUser.first_name;
user.lastName = telegramUser.last_name;
user.registeredAt = new Date();
}
return userRepository.save(user);
}
async updateUserSettings(telegramId: number, settings: Partial<Pick<User, 'language' | 'notifications'>>): Promise<void> {
const userRepository = this.connection.getRepository(User);
await userRepository.update({ telegramId }, settings);
}
} |
|
Независимо от выбранной базы данных, важно следовать принципам безопасности и эффективности:
1. Индексация критических полей: поля, используемые для поиска (например, telegramId), должны быть проиндексированы.
2. Валидация данных: перед сохранением данных в базу необходимо проверять их корректность.
3. Защита от SQL-инъекций: использовать параметризованные запросы и ORM вместо прямой конкатенации строк SQL.
4. Управление соединениями: правильно открывать и закрывать соединения с базой данных.
5. Миграции: использовать системы миграций для безопасного обновления схемы базы данных.
Для небольших ботов или тестирования можно использовать локальные решения, такие как SQLite:
| TypeScript | 1
2
3
4
5
6
7
8
9
| // Настройка для SQLite
export async function initializeDatabase(): Promise<Connection> {
return createConnection({
type: 'sqlite',
database: 'bot.sqlite',
entities: [User],
synchronize: true
});
} |
|
Выбор оптимальной базы данных должен основываться на требованиях конкретного проекта, включая ожидаемую нагрузку, типы хранимых данных, частоту операций чтения и записи, а также требования к масштабируемости.
Развертывание бота
После разработки и тестирования бота на локальной машине наступает этап его развертывания на рабочем сервере. Этот процесс критически важен для обеспечения стабильного и непрерывного функционирования бота. В отличие от обычных веб-приложений, Telegram-боты могут работать в двух режимах: long polling и webhook, что предоставляет разработчикам гибкость при выборе стратегии развертывания.
Long polling — это метод, при котором бот периодически запрашивает Telegram API на наличие новых сообщений. Этот подход не требует публичного IP-адреса или SSL-сертификата, что делает его удобным для быстрого старта:
| TypeScript | 1
2
| // Запуск бота в режиме long polling
bot.start(); |
|
Webhook, напротив, требует публичного HTTPS-эндпоинта, на который Telegram будет отправлять обновления. Этот метод более эффективен для высоконагруженных ботов, так как избавляет от необходимости постоянно опрашивать API:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
| // Настройка webhook
import express from 'express';
import { webhookCallback } from 'grammy';
const app = express();
app.use(express.json());
// Обработчик для webhook
app.use('/bot', webhookCallback(bot, 'express'));
// Запуск сервера
app.listen(process.env.PORT || 3000); |
|
Для надежного размещения бота существует несколько вариантов:
VPS (Virtual Private Server) — классический подход, предоставляющий полный контроль над окружением. Для развертывания бота на VPS потребуется:
1. Установить Node.js и npm.
2. Клонировать репозиторий с исходным кодом.
3. Установить зависимости с помощью npm install.
4. Скомпилировать TypeScript в JavaScript (npm run build).
5. Настроить процесс-менеджер, например, PM2, для обеспечения постоянной работы бота:
| Bash | 1
2
3
4
5
| # Установка PM2
npm install -g pm2
# Запуск бота через PM2
pm2 start dist/index.js --name telegram-bot |
|
Платформы PaaS (Platform as a Service) предлагают более простое развертывание без необходимости управления серверной инфраструктурой. Популярные варианты включают Heroku, Railway, Render и Vercel. Например, для Heroku процесс выглядит так:
1. Создать Procfile в корне проекта:
| TypeScript | 1
| web: node dist/index.js |
|
2. Настроить переменные окружения через панель управления Heroku.
3. Подключить репозиторий GitHub или использовать Heroku CLI для деплоя.
Serverless-платформы позволяют разворачивать боты без постоянно работающего сервера, что удобно для ботов с нерегулярной нагрузкой. Платформы вроде AWS Lambda, Vercel Functions или Cloudflare Workers хорошо подходят для этого сценария.
Независимо от выбранной платформы, критически важно правильно настроить переменные окружения (токен бота, API-ключи внешних сервисов) и обеспечить мониторинг работы бота для своевременного обнаружения проблем.
Мониторинг и логирование работы бота
Эффективный мониторинг и грамотное логирование — критически важные аспекты разработки промышленных Telegram-ботов. Без надлежащей системы наблюдения за работой бота практически невозможно диагностировать возникающие проблемы, оптимизировать производительность и обеспечивать бесперебойную работу сервиса. Для организации базового логирования в проекте на TypeScript можно использовать как встроенные средства, так и специализированные библиотеки. Самый простой подход — использование библиотеки Winston, которая предоставляет гибкие возможности для настройки логирования:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| import winston from 'winston';
// Создание логгера с несколькими транспортами
export const logger = winston.createLogger({
level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
defaultMeta: { service: 'telegram-bot' },
transports: [
// Консольный вывод для разработки
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
}),
// Запись в файл для важных событий
new winston.transports.File({
filename: 'logs/error.log',
level: 'error'
}),
// Общий лог всех событий
new winston.transports.File({ filename: 'logs/combined.log' })
]
}); |
|
Интеграция логгера с grammY осуществляется через middleware, который перехватывает все входящие сообщения и действия:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| bot.use(async (ctx, next) => {
const startTime = Date.now();
const user = ctx.from ? `${ctx.from.first_name} (${ctx.from.id})` : 'Неизвестный пользователь';
logger.info(`Получено обновление от ${user}`, {
updateType: ctx.updateType,
chatType: ctx.chat?.type,
messageText: ctx.message?.text
});
try {
// Выполняем следующие обработчики
await next();
// Логируем успешное выполнение и время обработки
const processingTime = Date.now() - startTime;
logger.debug(`Обновление обработано за ${processingTime}ms`);
} catch (error) {
// Логируем ошибку с полным контекстом
logger.error(`Ошибка при обработке обновления: ${error.message}`, {
error,
update: ctx.update,
processingTime: Date.now() - startTime
});
throw error; // Пробрасываем ошибку дальше для обработки в bot.catch()
}
}); |
|
Для мониторинга доступности и производительности бота можно использовать как простые решения, так и профессиональные системы. Базовый мониторинг доступности можно реализовать с помощью периодических проверок:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| import { CronJob } from 'cron';
// Проверка работоспособности бота каждые 5 минут
const healthCheck = new CronJob('*/5 * * * *', async () => {
try {
// Проверяем соединение с Telegram API
const botInfo = await bot.api.getMe();
logger.info('Проверка работоспособности успешна', { botInfo });
// Здесь можно добавить проверки соединения с базой данных
// и другими критическими компонентами
} catch (error) {
logger.error('Проверка работоспособности не удалась', { error });
// Отправка уведомления администратору
sendAlertToAdmin(`Бот не отвечает: ${error.message}`);
}
});
healthCheck.start(); |
|
Для более продвинутого мониторинга можно интегрировать системы вроде Prometheus и Grafana. Prometheus собирает метрики через HTTP-эндпоинт, который можно добавить к боту:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| import express from 'express';
import client from 'prom-client';
const app = express();
const prefix = 'telegram_bot_';
const collectDefaultMetrics = client.collectDefaultMetrics;
collectDefaultMetrics({ prefix });
// Создание кастомных метрик
const httpRequestDuration = new client.Histogram({
name: [INLINE]${prefix}http_request_duration_seconds[/INLINE],
help: 'Duration of HTTP requests in seconds',
labelNames: ['method', 'route', 'status_code']
});
// Счетчик обработанных сообщений
const messagesProcessed = new client.Counter({
name: [INLINE]${prefix}messages_processed_total[/INLINE],
help: 'Total number of messages processed',
labelNames: ['message_type']
});
// Экспорт метрик для Prometheus
app.get('/metrics', async (req, res) => {
res.set('Content-Type', client.register.contentType);
res.end(await client.register.metrics());
});
app.listen(3000, () => {
logger.info('Метрики доступны на порту 3000');
}); |
|
Важно настроить также алертинг, чтобы своевременно реагировать на проблемы в работе бота. Для этого можно использовать как простые решения с оповещениями по email или в Telegram, так и более комплексные системы вроде Alertmanager в связке с Prometheus.
Оптимизация производительности и масштабирование
По мере роста популярности Telegram-бота возрастает нагрузка на систему, и вопросы производительности и масштабирования становятся критически важными. Грамотная оптимизация позволяет не только ускорить работу бота, но и существенно снизить затраты на инфраструктуру, обеспечивая при этом высокий уровень пользовательского опыта. Ключевой компонент оптимизации производительности grammY-бота — выбор правильного режима получения обновлений. Библиотека предоставляет два основных механизма: long polling и webhook. Long polling является простым в настройке, но менее эффективным при высоких нагрузках. Webhook обеспечивает мгновенную доставку обновлений, но требует публично доступного HTTPS-эндпоинта:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
| import { Bot, webhookCallback } from 'grammy';
import express from 'express';
const bot = new Bot(process.env.TELEGRAM_BOT_TOKEN!);
const app = express();
// Настройка webhook для обработки обновлений
app.use('/webhook', express.json(), webhookCallback(bot, 'express'));
// Запуск нескольких экземпляров сервера
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Сервер запущен на порту ${PORT}`)); |
|
Для высоконагруженных ботов grammY предоставляет специализированный пакет @grammyjs/runner, который реализует продвинутые механизмы обработки обновлений:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
| import { Bot } from 'grammy';
import { run, sequentialize } from '@grammyjs/runner';
const bot = new Bot(process.env.TELEGRAM_BOT_TOKEN!);
// Гарантируем последовательную обработку сообщений от одного пользователя
bot.use(sequentialize((ctx) => ctx.from?.id.toString()));
// Запускаем бота с оптимизированным обработчиком
run(bot); |
|
Оптимальная обработка запросов к внешним API также критически важна. Реализация умного кеширования позволяет избежать повторных запросов к сторонним сервисам:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| import NodeCache from 'node-cache';
const cache = new NodeCache({ stdTTL: 300 }); // TTL: 5 минут
// Функция с кешированием результатов
async function getCachedWeather(city: string): Promise<WeatherData> {
const cacheKey = `weather:${city}`;
// Проверяем кеш
const cachedData = cache.get<WeatherData>(cacheKey);
if (cachedData) return cachedData;
// Если в кеше нет, делаем запрос к API
const weatherData = await weatherService.getWeather(city);
// Сохраняем в кеш
cache.set(cacheKey, weatherData);
return weatherData;
} |
|
Для масштабирования бота в условиях высоких нагрузок эффективной стратегией является горизонтальное масштабирование — запуск нескольких экземпляров бота за балансировщиком нагрузки. При таком подходе важно обеспечить единое хранилище данных для всех экземпляров:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
| import { session } from 'grammy';
import { RedisAdapter } from '@grammyjs/storage-redis';
import Redis from 'ioredis';
// Подключение к Redis для хранения сессий
const redis = new Redis(process.env.REDIS_URL);
const adapter = new RedisAdapter({ instance: redis });
// Настройка сессий с использованием Redis
bot.use(session({
initial: () => ({ counter: 0 }),
storage: adapter
})); |
|
Управление очередями сообщений позволяет контролировать нагрузку на бота. Библиотека Bull в сочетании с Redis обеспечивает надежное решение для обработки задач с высокой пропускной способностью:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| import Queue from 'bull';
// Создание очереди для обработки тяжелых задач
const processingQueue = new Queue('message-processing', process.env.REDIS_URL);
// Добавление задачи в очередь вместо синхронной обработки
bot.on('message:document', async (ctx) => {
await ctx.reply('Документ принят в обработку, результат придет позже');
// Добавляем задачу в очередь
await processingQueue.add({
fileId: ctx.message.document.file_id,
userId: ctx.from.id,
chatId: ctx.chat.id
});
});
// Обработчик очереди в отдельном процессе
processingQueue.process(async (job) => {
const { fileId, userId, chatId } = job.data;
// Здесь выполняется тяжелая обработка документа
const result = await processDocument(fileId);
// Отправляем результат пользователю
await bot.api.sendMessage(chatId, [INLINE]Обработка завершена: ${result}[/INLINE]);
}); |
|
При разработке масштабируемых ботов также важно уделять внимание архитектурным решениям: разделению обработчиков на микросервисы, использованию асинхронных операций и эффективной работе с памятью. TypeScript со строгой типизацией помогает выявить многие проблемы производительности на этапе компиляции, обеспечивая более надежный код.
Мониторинг производительности с помощью таких инструментов, как Prometheus и Grafana позволяет своевременно выявлять узкие места и оптимизировать их до того, как они станут критическими проблемами для пользователей бота.
Реализация CI/CD для непрерывной интеграции обновлений бота
После разработки и первичного развертывания бота возникает вопрос об организации процесса его обновления. CI/CD (Continuous Integration и Continuous Deployment) — методология, позволяющая автоматизировать тестирование и развертывание новых версий кода. Для Telegram-ботов, которые предоставляют сервис пользователям в режиме 24/7, правильно настроенный CI/CD является критически важным компонентом, обеспечивающим надежность и стабильность работы. GitHub Actions представляет собой удобный инструмент для построения CI/CD-пайплайнов непосредственно в репозитории проекта. Для базовой настройки необходимо создать файл конфигурации в директории .github/workflows:
| YAML | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
| name: Telegram Bot CI/CD
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: '18.x'
- name: Install dependencies
run: npm ci
- name: Lint code
run: npm run lint
- name: Run tests
run: npm test
env:
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN_TEST }}
deploy:
needs: test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Deploy to production
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /path/to/bot
git pull
npm ci
npm run build
pm2 restart telegram-bot |
|
Этот пример настраивает два этапа: тестирование и развертывание. При любом коммите в ветки main или develop автоматически запускаются тесты. Если изменения были в ветке main и тесты прошли успешно, бот автоматически обновляется на сервере.
Для повышения надежности процесса развертывания можно реализовать стратегию "синий-зеленый" (blue-green deployment), которая позволяет избежать простоев. Суть метода заключается в подготовке новой версии бота параллельно с работающей, а затем быстром переключении между ними:
| Bash | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| # Скрипт для blue-green развертывания
#!/bin/bash
# Определяем текущее активное окружение
CURRENT=$(pm2 jlist | jq -r '.[] | select(.name == "telegram-bot-active") | .pm_cwd')
# Определяем, какое окружение будет следующим
if [[ $CURRENT == *"blue"* ]]; then
NEXT="green"
else
NEXT="blue"
fi
# Обновляем код в неактивном окружении
cd /path/to/bot-$NEXT
git pull
npm ci
npm run build
# Запускаем бота в неактивном окружении
pm2 start ecosystem.config.js --only telegram-bot-$NEXT
# Проверяем работоспособность нового экземпляра
curl -s http://localhost:3001/health
# Если всё в порядке, меняем активное окружение
if [ $? -eq 0 ]; then
pm2 trigger telegram-bot-active switch-webhook
pm2 rename telegram-bot-$NEXT telegram-bot-active
pm2 stop telegram-bot-${CURRENT##*-}
echo "Успешное переключение на $NEXT"
else
echo "Ошибка при развертывании на $NEXT"
pm2 stop telegram-bot-$NEXT
exit 1
fi |
|
Для эффективной работы CI/CD необходимо также настроить автоматические тесты, которые проверяют основную функциональность бота. Jest в сочетании с mock-объектами Telegram API позволяет создавать надежные тесты без необходимости реальных вызовов API:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| // __tests__/commands.test.ts
import { Bot } from 'grammy';
import { MockAdapter } from 'grammy-test';
import { setupCommandHandlers } from '../src/handlers/commands';
describe('Command handlers', () => {
let bot: Bot;
let mockAdapter: MockAdapter;
beforeEach(() => {
bot = new Bot('test-token');
mockAdapter = new MockAdapter(bot);
setupCommandHandlers(bot);
});
test('start command should greet user', async () => {
const response = await mockAdapter.sendCommand('/start');
expect(response.text).toContain('Добро пожаловать');
});
}); |
|
Добавление автоматических уведомлений об успешности или неудаче деплоя завершает процесс CI/CD. Можно настроить отправку сообщений в Telegram-чат команды разработки:
| YAML | 1
2
3
4
5
6
7
8
9
10
11
| name: Send deploy notification
if: always()
uses: appleboy/telegram-action@master
with:
to: ${{ secrets.TELEGRAM_CHAT_ID }}
token: ${{ secrets.TELEGRAM_NOTIFICATION_BOT_TOKEN }}
message: |
Деплой ${{ github.repository }}:
Статус: ${{ job.status }}
Коммит: ${{ github.event.head_commit.message }}
Автор: ${{ github.actor }} |
|
Защита бота от спама и злоупотреблений
Популярные Telegram-боты неизбежно сталкиваются с проблемой злоупотреблений – от массовой отправки спама до целенаправленных атак на доступность сервиса. Грамотно реализованные механизмы защиты необходимы для обеспечения стабильной работы бота и предотвращения нежелательной активности.
Первой линией обороны против спама является ограничение частоты запросов (rate limiting). GrammY предоставляет для этой цели специальный плагин @grammyjs/ratelimiter:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
| import { limit } from '@grammyjs/ratelimiter';
// Ограничение: не более 3 сообщений за 5 секунд от одного пользователя
bot.use(limit({
timeFrame: 5000,
limit: 3,
keyGenerator: (ctx) => ctx.from?.id.toString(),
onLimitExceeded: (ctx) => {
ctx.reply('Пожалуйста, не отправляйте сообщения так часто!');
}
})); |
|
Для защиты от нежелательного контента можно внедрить фильтрацию сообщений, используя регулярные выражения или интеграцию с сервисами анализа текста:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
| bot.on('message:text', async (ctx, next) => {
const text = ctx.message.text.toLowerCase();
// Простая проверка на запрещенные слова
const blacklistedWords = ['спам', 'реклама', 'xxx'];
if (blacklistedWords.some(word => text.includes(word))) {
await ctx.reply('Ваше сообщение содержит запрещенный контент');
return; // Прерываем обработку
}
await next(); // Передаем управление следующему обработчику
}); |
|
Важным элементом защиты является реализация системы бана пользователей, нарушающих правила взаимодействия с ботом:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
| // Сервис для работы с баном пользователей
class BanService {
private bannedUsers: Set<number> = new Set();
isBanned(userId: number): boolean {
return this.bannedUsers.has(userId);
}
banUser(userId: number): void {
this.bannedUsers.add(userId);
}
unbanUser(userId: number): void {
this.bannedUsers.delete(userId);
}
}
const banService = new BanService();
// Middleware для проверки бана
bot.use(async (ctx, next) => {
if (ctx.from && banService.isBanned(ctx.from.id)) {
// Игнорируем сообщения от забаненных пользователей
return;
}
await next();
});
// Команда для администраторов
bot.command('ban', async (ctx) => {
// Проверка прав администратора
if (!isAdmin(ctx.from?.id)) {
return ctx.reply('У вас нет прав на использование этой команды');
}
// Извлекаем ID пользователя из сообщения
const match = ctx.message.text.match(/\/ban\s+(\d+)/);
if (!match) {
return ctx.reply('Укажите ID пользователя: /ban 123456789');
}
const userId = parseInt(match[1]);
banService.banUser(userId);
return ctx.reply(`Пользователь ${userId} заблокирован`);
}); |
|
Для защиты от DoS-атак и чрезмерной нагрузки эффективно использовать комбинацию rate limiting и отложенной обработки тяжелых задач через очереди:
| TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| import Queue from 'bull';
// Очередь для ресурсоемких операций
const processingQueue = new Queue('heavy-tasks');
bot.on('message:document', async (ctx) => {
// Вместо синхронной обработки добавляем задачу в очередь
await processingQueue.add({
fileId: ctx.message.document.file_id,
userId: ctx.from?.id,
chatId: ctx.chat?.id
});
await ctx.reply('Ваш документ принят в обработку');
}); |
|
Дополнительная защита может включать проверку на ботов с помощью CAPTCHA для критичных операций или мониторинг аномальной активности с автоматическим вводом временных ограничений.
Реализуя многоуровневую стратегию защиты, вы обеспечите надежную работу вашего Telegram-бота даже при высоких нагрузках и попытках злоупотребления его функционалом.
Разбиение скомилированного Typescript на файлы В проекте имеется множество typescript файлов, которые компилируются в один js. Но часть скриптов... Изучение TypeScript - советы Нуждаюсь в срочном освоении TypeScript. Поделитесь ресурсами, пожалуйста. Можно на русском и... Не понятен пример кода из спецификации TypeScript Читаю про объектные типы в спецификации на странцие 13, но не понятно из описания как устроен и... Передать свойство класса в анонимную функцию TypeScript как передать значение свойства класса в анонимную функцию.
например следующий код работает... TypeScript "PreComputeCompileTypeScript" how to fix in project Выскакивает ошибка
Везде исправление данной ошибки идёт путём редактирования файла ... Решение кольцевых зависимостей TypeScript + RequreJS Всем привет, возникла проблема с кольцевыми зависимостями при использовании наследования в... TypeScript | extends error Собственно, сделал такой класс:
class trueDate extends Date {
constructor(date: string)... Сохранение this в TypeScript Доброго дня. Подскажите пожалуйста, как можно сохранить this класса так, чтобы можно было... C# класс в TypeScript класс (перенос сущностей) делается бекенд на C#, фронтенд на ts, общаются через REST API (http)
сериализация обьектов... не могу настроить react + typescript в webstorm. Есть люди кто это сделал? Помогите, а то что то туплю уже и пробовал библиотеку скаченную подключать и ссылками. но так... Лучшие видео ресурсы о программировании, angualr 2, typeScript, react и все сомое вкусное Всем доброй поры времени.
Я нашел новый для себя, и как не странно, вообще новый ресур, с видео... Как проводится отладка при использовании TypeScript Присматриваюсь к Angular 2. Из прочитанных статей сложилось впечатление, что в мире Angular 2 есть...
|