> For the complete documentation index, see [llms.txt](https://delaney.gitbook.io/telegram-bot-na-js/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://delaney.gitbook.io/telegram-bot-na-js/master.md).

# Делаем телеграм бота на JS

![](/files/-MA5KyyilwrRoyBhO2pv)

## Вступление

### О чем будет это статья?

В данной статье я хочу поделиться опытом создания телеграм ботов. Сперва я покажу как можно настроить свое рабочее окружение, после чего преступим непосредственно к написанию самого бота.&#x20;

Пожалуйста, не воспринимайте меня как гуру *JavaScript* или библиотеки, которую мы будем использовать в процессе разработки. Я лишь хочу поделиться теми знаниями, которые сумел приобрести сам, и рассказать про подводные камни, с которыми столкнулся.

{% hint style="warning" %}
Те, кто знаком с NodeJS, можете пропустить или очень бегло просмотреть раздел "подготовки", где объясняется как создать приложение на ноде.

Также обращаю внимание, что я сижу на *MacOS* и использую команды для нее. Если у вас другая ОС либо же терминальные команды отличаются, попробуйте погуглить!
{% endhint %}

Больше информации вы можете узнать на канале [**Debug\_Yourself**](https://tg.guru/debug_u)**.**

Связаться с автором можно [**https://t.me/arutemu\_su**](https://t.me/arutemu_su)

## Подготовка

### Создаем проект на NodeJS

Как и было упомянуто выше, в качестве языка программирования мы выберем JS. Стоит ли знать его для того чтобы понять эту статью? Наверное **да**. Если вы не знаете JS хотя бы на уровне синтаксиса, многое будет вам непонятно, хотя я постараюсь осветить каждую строку кода по возможности. В любом случае никто вам не запрещает потратить пару часов/дней на то, чтобы познакомиться с JS и потом вернуться к этой статье.

Итак. Чтобы мы могли исполнять JS код на нашей машине, нам потребуется [NodeJS](https://nodejs.org/en/). Многие читатели наверняка слышали/знают о том, что это такое. Если коротко: *NodeJS* (нода) – это платформа, которая позволяет исполнять JS код на компьютере. Скачайте ее в зависимости от вашей ОС. Если после установки ввести команду в терминале

```
node -v
```

вы должны получить версию по типу `v14.2.0`.&#x20;

Теперь мы можем непосредственно создать проект на ноде. Для этого создайте папку на рабочем столе (или где вам удобнее) с названием проекта. Я назвал папку `debug_u_bot`.

Также нам потребуется редактор для написания кода. Какой выбирать – дело каждого. Я пользуюсь [Visual Studio Code](https://code.visualstudio.com/). Довольно хороший инструмент, можете попробовать.

Открываем редактор, а в нем и нашу созданную папку. Разумеется, сейчас там пусто.

![Пустой проект в VSC](/files/-MA5Te7yIVFXSMpgX5PV)

Вместе с нодой идет такой инструмент как **npm**. Сам по себе *npm* – это менеджер пакетов, с помощью которого вы можете устанавливать в свой проект различные сторонние зависимости. Для начала давайте при помощи него инициализируем проект. Откройте терминал в вашем редакторе (в VSC он находится в верхнем меню), либо же можете открыть терминал отдельно (только вам потребуется перейти непосредственно в вашу папку). Введите там команду&#x20;

```
npm init --yes
```

Этой командой вы инициализируете ваш проект, и в папке появится файл *package.json*. Часть команды `--yes` – это параметр, который автоматически пропускает вопросы по типу версии проекта, автора и т.п. Если запустить без него, то сможете ввести исходные данные, но в нашем случае в этом нет необходимости.

Итак. Если посмотреть на файл *package.json*, мы увидим следующее:

{% code title="package.json" %}

```javascript
{
  "name": "debug_u_bot",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}
```

{% endcode %}

&#x20;Сам по себе этот файл определяет настройки проекта, скрипты и зависимости, которые этот проект использует. Позже мы вернемся к нему.

### Создаем сервер на NodeJS

Теперь давайте создадим файл ***app.js*** в корне нашего каталога. Здесь мы будем писать наш будущий код. Однако прежде чем приступить к этому, нам необходимо установить несколько зависимостей. В терминале пишем команду

```bash
npm i express
```

Этой командой мы говорим *npm*, чтобы он установил пакет *express*. В данном случае `i` – сокращение от *install*. Вы также можете написать `npm install express` и получить аналогичный результат.

Что же такое *express*? Это минималистичный фреймворк для написаний NodeJS приложений. Он нам потребуется для быстрого создания сервера. Кому интересно, можете почитать про него [тут](https://expressjs.com/ru/).

![Как выглядит структура проекта](/files/-MA5iU8TT4YRMbWb4aeK)

Итак. Открываем наконец-таки файл *app.js* и пишем следующее

{% code title="app.js" %}

```javascript
const express = require('express')

const app = express()
const PORT = 3000

app.get('/', (req, res) => {
    res.send('Hello debug_Yourself')
})

app.listen(PORT, () => console.log(`My server is running on port ${PORT}`))
```

{% endcode %}

Коротко пробежимся что к чему: на первой строке мы добавляем модуль *'express'* и кладем его в переменную `express` На 3 строке мы инициализируем наше приложение и сохраняем в переменной `app`.  На 4 строке создаем переменную с портом, на котором будет заводиться наш сервер. На 6 строке создается обработчик для корневого запроса `'/'`, но об этом чуть ниже. На 10 строке мы запускаем наш HTTP сервер на заданном ранее порту `PORT`.&#x20;

Введите в терминале команду `node app` или `node app.js`и, если все сделано правильно, у вас запуститься локальный сервер. Также в терминале вы увидите следующее

![Запуск сервера на nodejs](/files/-MA5fn8-um8wCHfpF6it)

Если в браузере перейти по адресу [http://lcalhost:3000/](http://localhost:3000/) , то вы увидите&#x20;

![Результат обработчика app.get('/', ...)](/files/-MA5gGGm-prqBmD585cG)

Пока что мы все еще не пишем бота, однако уверенно готовимся к этому. Еще немного поработаем с нодой, так как именно она поможет запускать нашего дальнейшего бота.

В файле *app.js* попробуйте заменить текст, который выводится при обработке *get* запроса

{% code title="app.js" %}

```javascript
const express = require('express')

const app = express()
const PORT = 3000

// ИЗМЕНИТЕ ТЕКСТ В res.send()
app.get('/', (req, res) => {
    res.send('Hello. I changed this text')
})

app.listen(PORT, () => console.log(`My server is running on port ${PORT}`))
```

{% endcode %}

Теперь по идее если опять зайти на <http://localhost:3000/>, то текст должен поменяться, но этого не происходит. Причина кроется в том, что для того, чтобы наши изменения заимели силу, необходимо вручную перезапустить сервер. Сначала остановите его **(ctrl+c)**, а потом снова запустите командой `node app`. Теперь изменения вступили в силу, однако каждый раз вручную перезагружать сервер утомляет.&#x20;

Чтобы сервер сам автоматически перезагружался после каждого нашего изменения, давайте установим еще один пакет через терминал `npm i nodemon -g`. Это установит данный пакет глобально. ВАЖНО! У меня на MacOS для глобальной установки необходимо ставить через *sudo*, то есть `sudo npm i nodemon -g`. &#x20;

Теперь вы можете запустить проект в терминале командой `nodemon app`. Вновь измените текст на что-нибудь и сохраните файле *app.js*. Сервер рестартанет сам, и вы увидите изменения.

Последнее, что хотелось бы сделать, это внести пару изменений. Для импорта и экспорта файлов я предпочел бы использовать более современный синтаксис `import/from`.  Давайте изменим 1 строку файла *app.js* следующим образом:

{% code title="app.js" %}

```javascript
import express from 'express'

const app = express()
const PORT = 3000

app.get('/', (req, res) => {
    res.send('Hello. I changed this text again')
})

app.listen(PORT, () => console.log(`My server is running on port ${PORT}`))
```

{% endcode %}

Чтобы код работал, нам также необходимо кое-что добавить в файле ***package.json***

{% code title="package.json" %}

```javascript
{
  "name": "debug_u_bot",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "^4.17.1",
    "nodemon": "^2.0.4"
  },
  "type": "module"
}

```

{% endcode %}

а именно надо добавить еще одно поле *type* со значением *module*. Можете в конец через запятую вставить `"type": "module"` или просто скопировать мой код. Также давайте пропишем скрипт для более лаконичного запуска нашего приложения. На 6 строке `"scripts"` удалить *test* и замените его на `"start": "nodemon app.js"`.  Этим мы по сути команду *nodemon app.js* просто пакуем в команду *start*. Вот как должен выглядеть файл package.json теперь

{% code title="package.json" %}

```javascript
{
  "name": "debug_u_bot",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "nodemon app.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "^4.17.1",
    "nodemon": "^2.0.4"
  },
  "type": "module"
}

```

{% endcode %}

Теперь чтобы запустить проект, введите в терминал команду `npm run start`.&#x20;

## Создаем телеграм бота

### Что за бота мы будем писать?

В качестве примера мы напишем бота, в котором можно хранить задачи. Получиться своего рода **taskManagerBot** с минимальным функционалом. При это в процессе его создания мы сможем рассмотреть много базовых тем и приемов.

### Просим благословения у BotFather.

Всех ботов в тг надо создавать через отца всея ботов - **@BotFather**. Командой `/newbot`приступаем к созданию, введя имя (я ввел *debug\_u\_lesson*). После надо ввести *username* вашего бота. Разумеется, он должен быть уникальным для телеграма и заканчиваться на `bot`. Я ввел *debug\_u\_lesson\_bot*, вы придумайте что-то свое.&#x20;

Введя все этого, мы получаем наш заветный **token**, ради которого все и затевалось. Желательно его никому не показывать, ведь иначе кто-то другой сможет управлять вашим ботом. Хотя вы всегда можете поменять токен в *BotFather*. Там же вы можете немного кастомизировать бота, если напишите `/mybots`. Там можно изменять имя/описание/пикчу или подключать платежи для сбера/яндекса. Попробуйте изменить описание на что-то подобное "*Приветствую. Я учусь создавать ботов вместе с @debug\_u*".&#x20;

### Начинаем кодить

Несмотря на то, что проект предвещает быть простым и незамысловатым, предлагаю не мешать все в один файл *app.js*. Давайте такие вещи как порт сервера и токен телеграма мы будем хранить в отдельном файле. Сразу скажу, что в идеале их надо хранить в переменных окружения, но в нашем случае не хочу пока этим грузить. Если хотите почитать про то, что это значит, то [вот](https://medium.com/@hydrock/%D0%BF%D0%B5%D1%80%D0%B5%D0%BC%D0%B5%D0%BD%D0%BD%D1%8B%D0%B5-%D0%BE%D0%BA%D1%80%D1%83%D0%B6%D0%B5%D0%BD%D0%B8%D1%8F-%D0%B2-%D0%BF%D1%80%D0%B8%D0%BB%D0%BE%D0%B6%D0%B5%D0%BD%D0%B8%D0%B8-node-js-e9ca2131e6b6).

&#x20;В общем, создаем файл config.js в корне нашего проекта и пишем туда следующее:

{% code title="config.js" %}

```javascript
export const PORT = 3000
export const TOKEN = 'токен вашего бота'
```

{% endcode %}

Здесь мы просто объявляем две константы и командой `export` позволяем использовать их в других файлах/модулях.

Наш файл *app.js* немного видоизменится:

{% code title="app.js" %}

```javascript
import express from 'express'
import { PORT, TOKEN } from './config.js'

const app = express()

app.listen(PORT, () => console.log(`My server is running on port ${PORT}`))

```

{% endcode %}

Попробуйте еще раз запустить сервер командой `npm run start`. Если ошибок нет, значит все хорошо. Теперь нам надо установить саму библиотеку [Telegraf](https://telegraf.js.org/#/), которая упростит разработку бота. В терминале введите команду ниже и дождитесь установки.

```
npm install telegraf --save
```

Давайте научим бота приветствовать нас и что-то отвечать. Напишите следующее:

{% code title="app.js" %}

```javascript
import express from 'express'
import { PORT, TOKEN } from './config.js'
import Telegraf from 'telegraf'

const app = express()
const bot = new Telegraf(TOKEN)

bot.start(ctx => {
    ctx.reply('Welcome, bro')
})

bot.on('text', ctx => {
    ctx.reply('just text')
})

bot.launch()
app.listen(PORT, () => console.log(`My server is running on port ${PORT}`))

```

{% endcode %}

Давайте по порядку. На 6 строке мы создаем экземпляр класса `Telegraf` (в качестве параметра передаем наш ***TOKEN***) и кладем его в переменную `bot`. По сути `bot` – это объект, содержащий разные обработчики (*middlewares*) для обработки запросов. Что значат эти обработчики?

Взгляните на **8 строку**. Здесь мы пишем обработчик, который сработает, когда пользователь введет команду `/start`. То есть у объекта `bot` мы вызываем метод `start`, а в него передаем callback-функцию, которая принимает на вход один параметр - контекст (`ctx`), и на выходе отправляет сообщение '*Welcome, bro*'.&#x20;

Аналогично **строка 12.** Этот обработчик будет реагировать на любой текст, введенный пользователем. Почему именно текст? Потому что мы явно в качестве первого параметра метода `on` у объекта `bot` указываем *'text'*. Вторым параметром в `on` мы передаем callback-функцию, которая также принимает `ctx` и отвечает строкой *'just text'*.

Очень важно не забыть запустить бота на 16 строке, вызвав метод `launch()`.

Прежде, чем я познакомлю вас еще с некоторыми способами обработки, хочу обратить внимание. **Порядок написания обработчиков имеет значение**! Что я имею ввиду? Сейчас, если вы зайдете в своего бота и введете команду `/start`, то будете получать сообщение *'Welcome, bro'*, а если будете вводить любой текст, то *'just text'*. Теперь попробуйте поменять местами два этих обработчика и вы увидите, что введя команду `/start`, вы получите '*just text*'. И если вы введете любой текст, то получите этот же результат. Все потому, что обработчики проверяются сверху вниз и выполнится тот, который подходит под условие первым. Команда `/start`– это по сути тот же текст, поэтому срабатывает `bot.on('text')`. Убедившись в этом, можете вернуть порядок обработчиков обратно.

Давайте рассмотрим на что еще может реагировать бот? На самом деле на очень много вещей. Все их я сейчас физически не рассмотрю, но покажу принцип.

{% code title="app.js" %}

```javascript
import express from 'express'
import { PORT, TOKEN } from './config.js'
import Telegraf from 'telegraf'

const app = express()
const bot = new Telegraf(TOKEN)


bot.start(ctx => {
    ctx.reply('Welcome, bro')
})

bot.on('text', ctx => {
    ctx.reply('just text')
})

bot.on('voice', ctx => {
    ctx.reply('Какой чудный голос')
})

bot.on('sticker', ctx => {
    ctx.reply('Прикольный стикер')
})

bot.on('edited_message', ctx => {
    ctx.reply('Вы успешно изменили сообщение')
})

bot.launch()
app.listen(PORT, () => console.log(`My server is running on port ${PORT}`))

```

{% endcode %}

Мы можем передавать в метод `on` разные тригеры, на которые будет реагировать бот и делать что-то полезное. Пока же он нам просто отвечает сообщением, но позже мы это поправим. На строке 17, к примеру, обработчик реагирует на присланные нами войс-сообщения и говорит, что у нас чудный голос. На 21 строке аналогично, только реакция на стикер. На 25 строке немного интереснее: если мы изменим какое-либо свое сообщение, бот среагирует на это. И на самом деле таких тригеров много. Ваш редактор обязательно вам подскажет, что можно вставить.&#x20;

"А бот может реагировать не просто на сообщение, а на конкретный текст?" – спросите вы. Да. Смотрите:

{% code title="app.js" %}

```javascript
import express from 'express'
import { PORT, TOKEN } from './config.js'
import Telegraf from 'telegraf'

const app = express()
const bot = new Telegraf(TOKEN)


bot.start(ctx => {
    ctx.reply('Welcome, bro')
})

bot.hears('хочу есть', ctx => {
    ctx.reply('Так передохни и покушай')
})

bot.command('time', ctx => {
    ctx.reply(String(new Date()))
})

bot.launch()
app.listen(PORT, () => console.log(`My server is running on port ${PORT}`))

```

{% endcode %}

На 13 строке мы у объекта `bot` вызываем метод `hears()`, куда передаем конкретный текст, на который хотим, чтобы бот среагировал и обработал. Теперь, если вы напишите боту "*хочу есть*", получить вразумительный ответ.&#x20;

Также обратите внимание на строку 17. Если мы хотим обработать команду по типу `/start`, то вызываем метод `command`. В данном случае, если отправить боту `/time`, то получите полную дату и время.&#x20;

В принципе для начала этих обработчиков будет более, чем достаточно.

### Создаем клавиатуры

Давайте теперь рассмотрим клавиатуры, которые, кстати, хранить будем в файле *keyboards.js* (создайте его в корне вашего проекта). Клавиатуры могут быть как текстовые (при нажатии на кнопки вы отправляете боту текстовое сообщение) и инлайн (при нажатии вы отправляете *callback\_data*). На примерах вам станет все яснее.

Перейдите в созданный файл keyboards.js и импортируйте класс `Markup` вначале `import Markup from 'telegraf/markup'`. Все клавиатуры будут обернуты в функции, которые мы будем экспортировать. Давайте для начала сделаем клавиатуру, которая будет иметь кнопки "Мои задачи" для просмотра текущих заданий, "Добавить задачу" для добавления новых задач и "Смотивируй меня" для мотивации. Вот что вам необходимо написать:

{% code title="keyboards.js" %}

```javascript
import Markup from 'telegraf/markup.js'

export function getMainMenu() {
    return Markup.keyboard([
        ['Мои задачи', 'Добавить задачу'],
        ['Смотивируй меня']
    ]).resize().extra()
}
```

{% endcode %}

На 1 строке мы импортируем класс, предназначенный для клавиатур. На 3 строке создается функция `getMainMenu()`, которая возвращает клавиатуру. Важно, ее надо экспортировать командой `export`.

Так как нам нужно простое текстовое меню, мы на 4 строке вызываем у *Markup* метод `keyboard()`. В него надо передать один массив `[]`. А в этот массив мы уже передаем массивы с нашими кнопками. Каждый такой массив соответствует ряду. В данном случае у нас клавиатура в два ряда, где в первом 2 кнопки, во втором 1.&#x20;

На 7 строке мы вызываем метод `resize()`. Он отвечает за размерность кнопок. Если его убрать, то в моб. версии будет что-то подобное:

![Отображение клавиатуры на телефоне без resize()](/files/-MABIXiXSd92i0l1Yij4)

Также очень важно вызвать метод `extra()`, иначе клавиатура у вас просто не отобразится.&#x20;

Итак, клавиатура готова, теперь можно ее использовать. В файле *app.js* нам следует импортировать (4 строка) нашу функцию `getMainMenu()`. По логике нам надо, чтобы пользователь видел клавиатуру сразу, как только зашел в бота. Так как первым делом пользователь всегда запускает команду `/start`, то давайте отправлять клавиатуру в этом обработчике.

{% code title="app.js" %}

```javascript
import express from 'express'
import { PORT, TOKEN } from './config.js'
import Telegraf from 'telegraf'
import { getMainMenu } from './keyboards.js'

const app = express()
const bot = new Telegraf(TOKEN)

bot.start(ctx => {
    ctx.reply('Welcome, bro', getMainMenu())
})

bot.hears('хочу есть', ctx => {
    ctx.reply('Так передохни и покушай')
})

bot.command('time', ctx => {
    ctx.reply(String(new Date()))
})

bot.launch()
app.listen(PORT, () => console.log(`My server is running on port ${PORT}`))

```

{% endcode %}

Обработчик этой команды - это 9 строка. На 10 строке у объекта ctx мы вызываем метод `reply()`, который позволяет боту отвечать пользователю. И если первым параметром мы передаем туда текст ответа, то следующим мы можем отправить нашу клавиатуру. Наша функция как раз таки и возвращает клавиатуру, поэтому можем смело вызвать ее там.

Если вы все сделали верно, то при команде /start у вас появляется клавиатура, при нажатии на которую мы отправляем боту определенный текст.

На самом деле это и есть ее главная задумка. Мы хотим получать от пользователя конкретные сообщения, и чтобы он их не писал руками, мы даем возможность сделать это одним кликом. Более того, мы с вами уже умеем писать обработчика, который должен реагировать на определенный текст. Это метод `hears()`.&#x20;

К слову, метод `hears()` может принимать на вход не только строку, но и регулярное выражение и даже массив того и другого. Мы не будем сейчас усложнять процесс и для каждой кнопки клавиатуры напишем отдельные обработчики. Вот что у меня получилось:

{% code title="app.js" %}

```javascript
import express from 'express'
import { PORT, TOKEN } from './config.js'
import Telegraf from 'telegraf'
import { getMainMenu } from './keyboards.js'

const app = express()
const bot = new Telegraf(TOKEN)

bot.start(ctx => {
    ctx.reply('Welcome, bro', getMainMenu())
})

bot.hears('Мои задачи', ctx => {
    ctx.reply('Тут будут ваши задачи')
})

bot.hears('Добавить задачу', ctx => {
    ctx.reply('Тут вы сможете добавить свои задачи')
})

bot.hears('Смотивируй меня', ctx => {
    ctx.replyWithPhoto(
        'https://img2.goodfon.ru/wallpaper/nbig/7/ec/justdoit-dzhastduit-motivaciya.jpg',
        {
            caption: 'Не вздумай сдаваться!'
        }
    )
})

bot.launch()
app.listen(PORT, () => console.log(`My server is running on port ${PORT}`))

```

{% endcode %}

На 13 и 17 строках пока что написаны обработчики, которые лишь отвечают текстом, но вскоре мы это поправим. Куда интереснее строка 21. Этот обработчик отвечает нам не текстом, а фотографией с подписью. Так вы можете заметить, что у объекта **`ctx`** достаточно много методов и способов ответа. Первым параметром в `replyWithPhoto()` мы передаем *URL* фотографии (я взял просто из инета). Вторым параметром мы передаем объект с прочими настройками. В данном случае поле *caption* – это описание фото (просто текст).&#x20;

Давайте поработаем над логикой нашего бота. Задачи по-хорошему надо хранить в БД, но, опять же, не будем усложнять себе работу и сфокусируемся на боте. Пусть будет  массив, имитирующий нашу базу данных, куда будет сохраняться каждая задача. Сама по себе задача будет JS строкой.

Давайте создадим файл *db.js* также в корне нашего проекта, и там создадим массив `taskList`. Разумеется, не забываем его экспортировать.&#x20;

{% code title="db.js" %}

```javascript
export let taskList = []
```

{% endcode %}

Тепер в файле *app.js* напишем обработчик, который будет реагировать на любой текст.

Взгляните на код, ниже я поясняю все, что добавил.

{% code title="app.js" %}

```javascript
import express from 'express'
import { PORT, TOKEN } from './config.js'
import Telegraf from 'telegraf'
import { getMainMenu, yesNoKeyboard } from './keyboards.js'


const app = express()
const bot = new Telegraf(TOKEN)

bot.start(ctx => {
    ctx.replyWithHTML(
        'Приветсвую в <b>TaskManagerBot</b>\n\n'+
        'Чтобы быстро добавить задачу, просто напишите ее и отправьте боту',
        getMainMenu())
})

bot.hears('Мои задачи', ctx => {
    ctx.reply('Тут будут ваши задачи')
})

bot.hears('Добавить задачу', ctx => {
    ctx.reply('Тут вы сможете добавить свои задачи')
})

bot.hears('Смотивируй меня', ctx => {
    ctx.replyWithPhoto(
        'https://img2.goodfon.ru/wallpaper/nbig/7/ec/justdoit-dzhastduit-motivaciya.jpg',
        {
            caption: 'Не вздумай сдаваться!'
        }
    )
})

bot.on('text', ctx => {
    ctx.replyWithHTML(
        `Вы действительно хотите добавить задачу:\n\n`+
        `<i>${ctx.message.text}</i>`,
        yesNoKeyboard()
    )
})

bot.launch()
app.listen(PORT, () => console.log(`My server is running on port ${PORT}`))

```

{% endcode %}

Во-первых, на 10 строке я немного изменил обработку команды `/start`. Вместо простого метода `reply()`, я вызываю `replyWithHTML()`. Как несложно догадаться, он позволяет нам использовать некоторые теги *html*, чтобы преображать текст. В частности, тегами **\<b>** можно сделать текст жирным. Еще обратите внимание на часть строки `\n`. Это перевод текста на новую строку. Соответственно, двумя такими `\n` я перевожу на две строки вниз. Плюс, чтобы код помещался на экран, я строку разбил на две и просто произвожу конкатенацию строк оператором `+`.&#x20;

На 34 строке создан обработчик, реагирующий на любой текст. Напомню, что **последовательность обработчиков имеет значение**, поэтому он находится после всех. Логика работы здесь следующая: пользователь может что-то ввести по ошибке  или же передумает добавлять новую задачу. Для этого перед добавлением мы будем дублировать его задачу и спрашивать, уверен ли он? В качестве ответа мы будем отображать инлайн клавиатуру с выбором ответов "да /нет".&#x20;

Аналогично вызывается `replyWithHTML()`, где происходит конкатенация строк. Но важно отметить, что кавычки здесь ` `` ` специальные. В JS в подобные строки мы можем вставлять наши переменные путем вызова *${сюда наш объект}*, что и происходит на строке 37. Через объект `ctx` мы можем получить сообщение, которое пользователь отправил нам путем вызова `ctx.message.text`.&#x20;

На 38 строке передается клавиатура. Разумеется, ее необходимо создать в файле keyboards.js

{% code title="keyboards.js" %}

```javascript
import Markup from 'telegraf/markup.js'

export function getMainMenu() {
    return Markup.keyboard([
        ['Мои задачи', 'Добавить задачу'],
        ['Смотивируй меня']
    ]).resize().extra()
}

export function yesNoKeyboard() {
    return Markup.inlineKeyboard([
        Markup.callbackButton('Да', 'yes'),
        Markup.callbackButton('Нет', 'no')
    ], {columns: 2}).extra()
}
```

{% endcode %}

Инлайн клавы создаются несколько иначе. Во-первых, у *Markup* вызывается соответсвующий метод `inlineKeyboard()`. В него также передается массив, в который можно передавать как массив объектов кнопок, так и просто кнопки. В `callbackButton()` нужно передать текст кнопки и *callback\_data*. Эти 'yes' и 'no' будут являться триггерами при нажатии на них. Также вторым параметром в `inlineKeyboard()` мы можем передать кол-во колонок, которое нам надо. И не забываем вызвать метод `extra()`.

Сейчас, если боту что-то написать, как раз таки нам придет ответ типа&#x20;

![](/files/-MABpCOtGf2Hl8f6KIA0)

Как и отправляемый текст/войс/картинки/прочее, мы можем обрабатывать инлайн кнопки. Для этого у объекта `bot` необходимо вызывать метод `action()`. Взгляните на код:

{% code title="app.js" %}

```javascript
import express from 'express'
import { PORT, TOKEN } from './config.js'
import Telegraf from 'telegraf'
import { getMainMenu, yesNoKeyboard } from './keyboards.js'
import { addTask } from './db.js'


const app = express()
const bot = new Telegraf(TOKEN)



bot.start(ctx => {
    ctx.replyWithHTML(
        'Приветсвую в <b>TaskManagerBot</b>\n\n'+
        'Чтобы быстро добавить задачу, просто напишите ее и отправьте боту',
        getMainMenu())
})

bot.hears('Мои задачи', ctx => {
    ctx.reply('Тут будут ваши задачи')
})

bot.hears('Добавить задачу', ctx => {
    ctx.reply('Тут вы сможете добавить свои задачи')
})

bot.hears('Смотивируй меня', ctx => {
    ctx.replyWithPhoto(
        'https://img2.goodfon.ru/wallpaper/nbig/7/ec/justdoit-dzhastduit-motivaciya.jpg',
        {
            caption: 'Не вздумай сдаваться!'
        }
    )
})

bot.on('text', ctx => {
    ctx.session.taskText = ctx.message.text

    ctx.replyWithHTML(
        `Вы действительно хотите добавить задачу:\n\n`+
        `<i>${ctx.message.text}</i>`,
        yesNoKeyboard()
    )
})

bot.action(['yes', 'no'], ctx => {
    if (ctx.callbackQuery.data === 'yes') {
        addTask('сюда будем передавать текст задачи')
        ctx.editMessageText('Ваша задача успешно добавлена')
    } else {
        ctx.deleteMessage()
    }
})

bot.launch()
app.listen(PORT, () => console.log(`My server is running on port ${PORT}`))

```

{% endcode %}

На 47 строке мы пишем обработчик наших *callback\_data*. Так как кода на обработку будет мало, мы вместо того, чтобы писать два отдельных обработчика под '*yes*' и '*no*', создадим один, куда можно передать массив строк. Однако, в зависимости от того, какая кнопка была нажата, нам надо совершить разные деяния. Если это было "*no*", то мы будем удалять последнее сообщение, которое прислал бот. То есть это будет как раз сообщение с вопросом о добавлении задачи. Если это было "*yes*", то нам надо добавить пользовательское сообщение (его задачу) в массив *taskList* (который находится в файле *db.js*). После можно отредактировать последнее сообщение, присланное ботом, сказав пользователю, что его задача успешно добавлена.

Как нам получить параметры кнопок? Все просто. Если к объекту *ctx* обратиться так `ctx.callbackQuery.data`, то можно получить значение той кнопки, которая была нажата.

Вы спросите, что за функция `addTask()`на 49 строке? И это справедливый вопрос. Это будет своего рода имитация добавления данных в реальную базу данных. Разумеется, функцию надо написать в файле *db.js* и импортировать в *app.js* (строка 5).

{% code title="" %}

```javascript
let taskList = []

export function addTask(text) {
    taskList.push(text)
}
```

{% endcode %}

На 3 строке мы объявляем функцию и экспортируем ее. Функция принимает параметр *text* – текст сообщения, которое пользователь отправляет боту. На 4 строке мы добавляем *text* в массив методой `push()`.

Теперь возникает небольшая головоломка. Когда пользователь отправляет боту сообщение, имеется один контекст *ctx*. Мы можем спокойно получить текст его сообщения командой `ctx.message.text`. Однако после этого пользователь жмет на инлайн кнопки "да/нет", и в таком случае контекст перезаписывается. То есть, если сейчас попробовать посмотреть текст сообщения (как я показал выше) в обработчике на 47 строке (файл *app.js*), то вы получите ошибку. Как быть?

Мне на ум приходит следующее: пока `ctx` имеет пользовательское сообщение, можно сохранит его глобально и потом получить там, где надо. Благо *Telegraf* любит разработчиков и дает такую возможность. Мы можем подключить "сессии", тем самым, будет возможность хранить что-то глобально. Для этого нужно написать следующее:

{% code title="app.js" %}

```javascript
import express from 'express'
import { PORT, TOKEN } from './config.js'
import Telegraf from 'telegraf'
import session from 'telegraf/session.js'
import { getMainMenu, yesNoKeyboard } from './keyboards.js'
import { addTask } from './db.js'

const app = express()
const bot = new Telegraf(TOKEN)

bot.use(session())

bot.start(ctx => {
    ctx.replyWithHTML(
        'Приветсвую в <b>TaskManagerBot</b>\n\n'+
        'Чтобы быстро добавить задачу, просто напишите ее и отправьте боту',
        getMainMenu())
})

bot.hears('Мои задачи', ctx => {
    ctx.reply('Тут будут ваши задачи')
})

bot.hears('Добавить задачу', ctx => {
    ctx.reply('Тут вы сможете добавить свои задачи')
})

bot.hears('Смотивируй меня', ctx => {
    ctx.replyWithPhoto(
        'https://img2.goodfon.ru/wallpaper/nbig/7/ec/justdoit-dzhastduit-motivaciya.jpg',
        {
            caption: 'Не вздумай сдаваться!'
        }
    )
})

bot.on('text', ctx => {
    ctx.session.taskText = ctx.message.text

    ctx.replyWithHTML(
        `Вы действительно хотите добавить задачу:\n\n`+
        `<i>${ctx.message.text}</i>`,
        yesNoKeyboard()
    )
})

bot.action(['yes', 'no'], ctx => {
    if (ctx.callbackQuery.data === 'yes') {
        addTask(ctx.session.taskText)
        ctx.editMessageText('Ваша задача успешно добавлена')
    } else {
        ctx.deleteMessage()
    }
})

bot.launch()
app.listen(PORT, () => console.log(`My server is running on port ${PORT}`))

```

{% endcode %}

На 4 строке мы импортируем *session*. Так как это *middleware*, то его необходимо подключить до всех обработчиков, что мы и делаем на 11 строке. Думаю, вы помните, что на пользовательский текст срабатывает обработчик на строке 37. Поэтому в нем, на 38 строке мы создаем переменную `ctx.session.taskText` и кладем в нее текст сообщения `ctx.message.text`. Теперь мы сможем вызывать `ctx.session.taskText` почти везде, где захотим. К слову, любую переменную сессии вы можете создать по форме `ctx.session.<название переменной>`.

Наконец, на 49 строке мы передаем эту переменную в качестве параметра функции `addTask`. Если вы все сделали правильно, то добавление должно работать, правда визуально мы не можем посмотреть задачи. Можете в функцию *addTask* в самом конце добавить `console.log(taskList)` и посмотреть в консоле.

### Добавляем возможность посмотреть задачи

Как я и говорил, наш файл *db.js* служит некой имитацией работы с реальной базой. Однако в жизни запросы к БД – это асинхронные операции, которые могут занимать некоторое время. Это значит, к примеру, если мы пишем запрос типа *getDataFromDB()*, то он не выполнится именно в тот момент, когда будет вызван. Вдруг там запрос на получение миллиона записей, и он может занять пару секунд. Если весь оставшийся код будет ждать, пока выполнится такой запрос, будет не очень круто. И уже тем более, если будет ждать сам пользователь.&#x20;

В общем давайте добавим функцию, которая позволит получить данные массива *taskList*.&#x20;

{% code title="db.js" %}

```javascript
let taskList = []

export function getMyTasks() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(taskList)
        }, 500)
    })
}

export function addTask(text) {
    taskList.push(text)
}
```

{% endcode %}

На 3 строке создаем и экспортируем функцию `getMyTasks()`. Эта функция будет возвращать так называемый промис, почитать о которых вы [можете здесь](https://learn.javascript.ru/promise). В промис передается функция обратного вызова с одним параметром *resolve*. Внутри промиса также вызывается функция `setTimeout()`. Она позволяет вызывать какую-либо функцию через определенный интервал. Пусть это будет **500** миллисекунд. А вызывать она будет `resolve()` с нашим массивом *taskList*. Здесь довольно много кода, но весь он направлен на то, чтобы мы смогли асинхронно получить массив с задачами через 500 миллисекунд.

Теперь надо нашу функцию импортировать в *app.js* и вызывать в соответствующем обработчике.

{% code title="app.js" %}

```javascript
import express from 'express'
import { PORT, TOKEN } from './config.js'
import Telegraf from 'telegraf'
import session from 'telegraf/session.js'
import { getMainMenu, yesNoKeyboard } from './keyboards.js'
import { getMyTasks, addTask } from './db.js'

const app = express()
const bot = new Telegraf(TOKEN)

bot.use(session())

bot.start(ctx => {
    ctx.replyWithHTML(
        'Приветсвую в <b>TaskManagerBot</b>\n\n'+
        'Чтобы быстро добавить задачу, просто напишите ее и отправьте боту',
        getMainMenu())
})

bot.hears('Мои задачи', async ctx => {
    const tasks = await getMyTasks()
    let result = ''

    for (let i = 0; i < tasks.length; i++) {
        result = result + `[${i+1}] ${tasks[i]}\n`
    }

    ctx.replyWithHTML(
        '<b>Список ваших задач:</b>\n\n'+
        `${result}`
    )
})

bot.hears('Добавить задачу', ctx => {
    ctx.reply('Тут вы сможете добавить свои задачи')
})

bot.hears('Смотивируй меня', ctx => {
    ctx.replyWithPhoto(
        'https://img2.goodfon.ru/wallpaper/nbig/7/ec/justdoit-dzhastduit-motivaciya.jpg',
        {
            caption: 'Не вздумай сдаваться!'
        }
    )
})

bot.on('text', ctx => {
    ctx.session.taskText = ctx.message.text

    ctx.replyWithHTML(
        `Вы действительно хотите добавить задачу:\n\n`+
        `<i>${ctx.message.text}</i>`,
        yesNoKeyboard()
    )
})

bot.action(['yes', 'no'], ctx => {
    if (ctx.callbackQuery.data === 'yes') {
        addTask(ctx.session.taskText)
        ctx.editMessageText('Ваша задача успешно добавлена')
    } else {
        ctx.deleteMessage()
    }
})

bot.launch()
app.listen(PORT, () => console.log(`My server is running on port ${PORT}`))

```

{% endcode %}

Обработчик на 20 строке претерпел изменения! Во-первых, наша *callback*-функция с одним параметром *ctx* заимела оператор *async* и выглядит теперь `async ctx => {...}`. Подробнее про ***async/await*** вы сможете [почитать здесь](https://tproger.ru/translations/understanding-async-await-in-javascript/). Я лишь скажу, что эти операторы упрощают работу с асинхронным кодом. Так как функция для получения всех задач возвращает промис, то нам надо его обработать. Для обработки промисов есть несколько подходов, но в нашем примере будет использоваться оператор `await`. Однако чтобы использовать этот оператор внутри функций, сами функции должны быть явно объявлены с оператором *async*. По этой причине он и был добавлен.

На 21 строке мы вызываем функцию `getMyTasks()` с оператором *await* (конечно, не забываем импортировать ее на строке 6), что дает нам на выходе массив, который мы сохраняем в переменную `tasks`.&#x20;

Так как показывать пользователю массив не лучший вариант, мы каждый его элемент будем в виде строки сохранять в одну общую строку `result` (создаем на 22 строке).  Для того, чтобы пробежаться по массиву в JS, можно использовать простой цикл ***for***. Чтобы каждая задача имела свой уникальный номер, мы просто будем выводить ее индекс из массива задач, увеличенный на единицу (увеличиваем т.к. индексация начинается с нуля, а не единицы).

После чего на 28 строке при помощи `replyWithHTML()` выводим весь список задач. Попробуйте запустить сервер, добавьте пару задач и нажмите на "Мои задачи". Если все работает, то поздравляю вас! Осталось только сделать удаление задач и на этом можно заканчивать.

### Удаление задач

Кнопка "Добавить задачу" по факту оказалась бесполезной. Предлагаю заменить ее на "Удалить задачу". Для этого поменяйте текст в файле *keyboards.js*, а также текст обработчика на 34 строке в файле *app.js*. Чтобы изменения вступили в силу, необходимо в боте написать `/start`.

Пусть логика будет простая: удалить задачу можно будет только если отправить боту сообщение "*удалить <порядковый номер задачи>*". При нажатии на кнопку "Удалить задачу" мы будем сообщать пользователю об той инструкции. Выглядеть этот обработчик будет так:

{% code title="app.js" %}

```javascript
// some code

bot.hears('Удалить задачу', ctx => {
    ctx.replyWithHTML(
        'Введите фразу <i>"удалить `порядковый номер задачи`"</i>, чтобы удалить сообщение,'+
        'например, <b>"удалить 3"</b>:'
    )
})

// some code
```

{% endcode %}

Для определения ключевой фразы удаления задач можно использовать регулярные выражения. Пусть после обработчика "Удалить задачу" будет новый обработчик, который будет реагировать на нужный нам текст.

{% code title="app.js" %}

```javascript
// some code

bot.hears('Удалить задачу', ctx => {
    ctx.replyWithHTML(
        'Введите фразу <i>"удалить `порядковый номер задачи`"</i>, чтобы удалить сообщение,'+
        'например, <b>"удалить 3"</b>:'
    )
})

bot.hears(/^удалить\s(\d+)$/, ctx => {
    const id = Number(+/\d+/.exec(ctx.message.text)) - 1
    deleteTask(id)
    ctx.reply('Ваша задача успешно удалена')
})

// some code
```

{% endcode %}

На 10 строке у нас идет как раз таки регулярное выражение. На 11 строке мы берем `ctx.message.text`, где содержится текст, к примеру, "*удалить 2*". Чтобы из него вычленить цифру, а именно она нам и нужна, используется еще одно регулярное выражение `+/\d+/.exec()`. Но эта цифра имеет строковый тип, а нам нужен целочисленный, поэтому мы преобразовываем при помощи `Number()`. И теперь от числа можем смело отнимать единицу. Это нужно затем, что выше мы прибавляли единицу, чтобы нумерация задач для пользователя отображалась с 1. Индексы же массива начинаются с нуля, и чтобы правильно по ним удалять, нужно снова отнять единицу. Весь полученный результат сохраняется в переменную `id`.

На 12 строке вызывается метод `deleteTask()`, в который надо передать id. Разумеется, его надо написать. Для этого в файле *db.js* добавим следующее:

{% code title="db.js" %}

```javascript
let taskList = []

export function getMyTasks() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(taskList)
        }, 500)
    })
}

export function addTask(text) {
    taskList.push(text)
}

export function deleteTask(id) {
    taskList.splice(id, 1)
}
```

{% endcode %}

На 15 строке создается и экспортируется функция для удаления, которая на вход принимает параметр `id`. В нем просто идет вызов метода `splice()`, который позволяет удалять элементы у массивов в JS.&#x20;

После создания этого метода не забудьте импортировать его в файле *app.js*. Итоговый вариант должен выглядеть так:

{% code title="app.js" %}

```javascript
import express from 'express'
import { PORT, TOKEN } from './config.js'
import Telegraf from 'telegraf'
import session from 'telegraf/session.js'
import { getMainMenu, yesNoKeyboard } from './keyboards.js'
import { getMyTasks, addTask, deleteTask } from './db.js'

const app = express()
const bot = new Telegraf(TOKEN)

bot.use(session())

bot.start(ctx => {
    ctx.replyWithHTML(
        'Приветсвую в <b>TaskManagerBot</b>\n\n'+
        'Чтобы быстро добавить задачу, просто напишите ее и отправьте боту',
        getMainMenu())
})

bot.hears('Мои задачи', async ctx => {
    const tasks = await getMyTasks()
    let result = ''

    for (let i = 0; i < tasks.length; i++) {
        result = result + `[${i+1}] ${tasks[i]}\n`
    }

    ctx.replyWithHTML(
        '<b>Список ваших задач:</b>\n\n'+
        `${result}`
    )
})

bot.hears('Удалить задачу', ctx => {
    ctx.replyWithHTML(
        'Введите фразу <i>"удалить `порядковый номер задачи`"</i>, чтобы удалить сообщение,'+
        'например, <b>"удалить 3"</b>:'
    )
})

bot.hears(/^удалить\s(\d+)$/, ctx => {
    const id = Number(+/\d+/.exec(ctx.message.text)) - 1
    deleteTask(id)
    ctx.reply('Ваша задача успешно удалена')
})

bot.hears('Смотивируй меня', ctx => {
    ctx.replyWithPhoto(
        'https://img2.goodfon.ru/wallpaper/nbig/7/ec/justdoit-dzhastduit-motivaciya.jpg',
        {
            caption: 'Не вздумай сдаваться!'
        }
    )
})

bot.on('text', ctx => {
    ctx.session.taskText = ctx.message.text

    ctx.replyWithHTML(
        `Вы действительно хотите добавить задачу:\n\n`+
        `<i>${ctx.message.text}</i>`,
        yesNoKeyboard()
    )
})

bot.action(['yes', 'no'], ctx => {
    if (ctx.callbackQuery.data === 'yes') {
        addTask(ctx.session.taskText)
        ctx.editMessageText('Ваша задача успешно добавлена')
    } else {
        ctx.deleteMessage()
    }
})

bot.launch()
app.listen(PORT, () => console.log(`My server is running on port ${PORT}`))

```

{% endcode %}

## Заключение

Друзья! Могу вас поздравить с вполне рабочим ботом. Давайте подведем итог, что было сделано: Вы научились настраивать свое рабочее окружение (использовать ноду для своих JS проектов). Что касается ботов, то вы узнали про несколько способов обработки пользовательских действий. Научились создавать два типа клавиатур. Поработали с асинхронностью и регулярными выражениями. Также посмотрели, как можно отправить фотографии с описанием. Осталось, разве что только задеплоить бота на рабочий сервер, но об этом в другой статье.

С практической точки зрения бот не очень много пользы несет. Однако его создание позволило **охватить много базовых подходов**, используемых при разработке телеграм ботов. С этими знаниями вы уже можете создавать свои простые и рабочие решения.&#x20;

Исходники бота на [**гитхабе**](https://github.com/artskadin/debug_u_lessons/tree/master/taskManagerBot).

Будем надеятся, что это не последняя моя статья. Если она оказалась для вас полезной или же остались какие-либо вопросы, вы всегда можете связаться со мной в телеграме  [**arutemu\_su**](https://t.me/arutemu_su)**.**

Написано специально для телеграм канала [**Debug\_Yourself**](https://t.me/debug_u)**.** Можете подписаться на канал, ведь там я рассказываю про свой путь становления программистом. Делюсь информацией, которую изучаю, проектами, которые делаю, шишками, которые набиваю. Также иногда проскакивает диванная философия.
