Skip to content
junioraww edited this page Nov 16, 2025 · 2 revisions

Keygram

Все, что нужно знать о библиотеке:

  1. Как работают хэндлеры (обработчики событий)
  2. Как работают клавиатуры
  3. Как использовать состояния

Handlers

Handler (или Middleware) обрабатывает запросы от Telegram, а Telegram в свою очередь перенаправляет нам запросы от пользователей.

Отличие Handler от Middleware в том, что Handler обрабатывает определенное событие (например, пользователь отправил текст), а Middleware обрабатывает все сразу (например, для логирования)

Telegram Bot API предлагает класс Update как универсальный объект для всех запросов. Когда пользователь пишет сообщение, Telegram присылает нашему боту JSON-запрос вида:

{
  "update_id": 12345,
  "message": {
    ...
  } 
}

Внутри message — все, что может содержать сообщение. Например, если пользователь отправил просто текст, то message выглядит примерно так:

{
  "message_id": 12345,
  "chat": {
    "id": 2281337
  },
  "from": {
    "id": 666228,
    "first_name": "Maxim"
  },
  "text": "Привет!"
}

Если мы хотим, чтобы бот реагировал на текстовые сообщения, мы напишем следующий код:

import { TelegramBot } from 'keygram'

const bot = new TelegramBot("токен_от_@botfather")

bot.on('message:text', ctx => {
  return ctx.reply("Привет мир!")
})

bot.startPolling()

Это самая простая программа, что мы можем сделать.

Здесь bot.on(...) — это добавление Handler, bot.startPolling() — начало цикла для обработки запросов.

После bot.startPolling() мы начинаем активно "опрашивать" сервера Telegram о том, не написали ли нам новые сообщения.

Чтож, поскольку при открытии бота с любого клиента Telegram нам сначала предлагают написать команду /start, мы сделаем обработку самого первого сообщения особенной:

bot.text('/start', ctx => {
  return ctx.reply("Приветствую вас!")
})

bot.on('message:text', ctx => {
  return ctx.reply("Понял, принял!")
})

Теперь при попытке написать /start наш примитивный бот ответит по-особому.

Замечу, что в данной библиотеке нет аргумента next(), как во многих библиотеках Telegram под JS, работающих с Middleware. По умолчанию все добавленные обработчики (Handler) будут продолжать выполнение, то есть next(). Для того, чтобы завершить выполнение в Handler, мы должны вернуть в функции неложное значение. В примере выше мы возвращаем ctx.reply(), что всегда является неложным значением (не undefined, 0, null и так далее)

Все доступные параметры для Update можно найти здесь Все доступные параметры для Message можно найти здесь

Для упрощения разработки, некоторые параметры Message можно передавать без указания message:

bot.on("video", handleVideo)
bot.on("new_chat_member", handleNewbie)
// НО:
bot.on("poll", handlePoll) // <-- Сработает иначе

В документации Telegram Bot API можно найти, что Poll является объектом для Update, который мы получаем при новых голосах в отправленных ботом poll'ах.

Если в нашем коде мы хотим слушать именно сообщения с poll, а не голоса, мы напишем bot.on("message:poll", handlePoll)

Panel & Keyboard

Когда боты только появлялись, все использовали команды. Теперь же все мессенджеры повсеместно ввели клавиатуры, и мы можем использовать их для создания сценариев.

import { Panel, Keyboard } from 'keygram'

const sendClick = ctx => {
  ctx.reply("Клик!")
}

const startKeyboard = Panel().Callback("Нажми меня", sendClick)

Примерно так выглядит создание клавиатуры, прикрепленной к сообщению. Чтобы отправить ее, мы можем написать:

ctx.reply("Сообщение с клавиатурой", startKeyboard)
ctx.reply("Сообщение с объектом", { keyboard: startKeyboard })
ctx.reply({ text: "Сообщение", keyboard: startKeyboard })

Все три варианта корректно обрабатываются библиотекой.

Чтобы отправить клавиатуру, прикрепленную не к сообщению, а к низу экрана, используйте Keyboard() вместо Panel()

Как это устроено под капотом?

Когда инициализируется клавиатура (Panel()) и добавляются кнопки Callback(), библиотека сохраняет функции и вписывает их названия в callback_data. Когда клиент жмет кнопку, бот вытаскивает название функции из callback_data и выполняет ее.

По умолчанию, чтобы частично исключить поддельные запросы, в каждый callback_data записывается крипто-сигнатура (сформированная из названия функции, передаваемых аргументов и токена бота). Однако это не спасает от повторной отправки тех же запросов, поэтому проверки обязательны.

States

Когда нам нужно нажать кнопку, а затем ввести текст, то управлять сценарием становится сложно. В библиотеке реализован StateManager для управления состояниями и блокировками Handler'ов определенным пользователям (во время взаимодействий).

Но изначально мы должны настроить сохранение State между перезапусками программы (бота):

// [ПРИМЕР - Не использовать в проде!]
import { createClient } from "redis"

const redis = await createClient()

bot.state.save = async (ctx, new_state) => {
  // Ваша функция сохранения состояния в базу данных
  await redis.set('user:' + ctx.from.id, JSON.stringify(new_state))
  // TODO: обработка ошибок (задача для вас!)
}

bot.state.load = async (ctx) => {
  // Ваша функция загрузки состояния из базы данных в кеш библиотеки
  return JSON.parse(await redis.get('user:' + ctx.from.id) || '{}')
}

// bot.state.setMaxSize(100) // Макс. количество загруженных состояний
// bot.state.setUnloadAfter(60) // Сколько секунд пройдет до отгрузки состояния из кеша библиотеки

Данный код выполняет сохранение и загрузку состояний в базу данных. Вот пример использования состояний:

function onInput(ctx) {
  if (!ctx.text) return ctx.reply("Введите текст!")
  ctx.reply("Сохранено! Вы ввели: " + ctx.text)
  ctx.state = { input: ctx.text }
}

bot.on('/input', async ctx => {
  ctx.state = { allow: [] } // или { allow: '' }
  ctx.input(ctx)
})

bot.on('/show', async ctx => {
  const state = ctx.state || { input: null }
  ctx.reply("Сохраненное значение: " + state.input)
})

bot.register(onInput) // Не забываем, чтобы ввод работал после перезапуска!

Когда мы пишем ctx.state = { allow: [] }, мы блокируем все Handler'ы, которые могли бы помешать ожиданию ввода await ctx.input(). Например, мы не обрабатываем /show, повторный /input, нажатия кнопок и так далее.

Поскольку функция получения состояния может быть (и в проде — должна быть) асинхронной, то установка/получение ctx.state в синхронном варианте может вызвать гонку состояний. Поэтому корректнее использовать await bot.setState(...), но для простоты примера асинхронность не задействована.

Если мы хотим разрешить, например, нажатие кнопки с функцией cancelInput, то сделали бы так:

function cancelInput(ctx) {
  ctx.reset() // То же самое, что и ctx.state = {}
  ctx.answer("Отменено!")
}

const cancelKeyboard = Keyboard().Callback("Отмена", cancelInput)

function onInput(ctx) {
  if (!ctx.text) return ctx.reply("Введите текст!")
  ctx.reply("Сохранено! Вы ввели: " + ctx.text)
  ctx.state = { input: ctx.text }
}

bot.on('/input', async ctx => {
  ctx.state = { allow: 'cancelInput' } // Если более 1 функции - пишем в []
  ctx.reply("Введите текст", cancelKeyboard)
  ctx.input(onInput) // input завершится после любого изменения State (даже не null)
})

bot.register(onInput, cancelInput) // Не забываем, чтобы кнопка работала после перезапуска!

ctx.input() добавляет параметр input: [название_функции] в State для того, чтобы продолжить ввод после перезапуска программы (бота).

Функцию можно немного упростить:

bot.on('/input', async ctx => {
  ctx.reply("Введите текст", cancelKeyboard)
  ctx.input(onInput, 'cancelInput') // Второй аргумент ctx.input() может принимать allow
  // или объект для вставки в ctx.state (зависит от того, что вы указали - если массив/строка, то это allow)
})

Clone this wiki locally