Роутинг в комплексных чат-ботах с Hobot framework

Начав разрабатывать боты для Telegram несколько лет назад, я открыл для себя производительность, простоту и гибкость работы с ними как с частным случаем интерфейса командной строки. Эти характеристики, доступные сегодня многим — во многом заслуга популярного фреймворка telegraf.js и ему подобным, которые предоставляют упрощенные методы для работы с API Telegram.

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

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

Немного базовых сведений

В чат-ботах и CLI выполнение одного логического действия часто состоит из нескольких шагов-уточнений или шагов-разветвлений. Это требует от программы хранения некой координаты, чтобы помнить в каком месте в флоу находится пользователь и исполнения его команды в соответствии с этой координатой.

Самая простая иллюстрация — выполнение команды npm init, в ходе которой программа просит вас по очереди указать те или иные данные для package.json.

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

Мы называем эту переменную path — путь, к которому логически привязывается тот или иной код. Она нужна всегда, если в боте есть навигация или команды, отдаваемые в несколько шагов.

Сегодняшняя практика в архитектуре ботов

Подход, подсмотренный мной вначале у других разработчиков, выглядел так: для любого обновления, приходящего от пользователя пишется список проверок на то или иное значение переменной path и внутрь этих проверок помещается бизнес-логика и дальнейшая навигация в самом элементарном виде:

onUserInput(ctx, input) {     switch(ctx.session.path) {         case 'firstPath': 	    if (input === 'Привет!') {                // Бизнес-логика и ответ 	        ctx.reply('Привет!'); 	        ctx.session.path = 'secondPath'; 	    } else { 	        ctx.reply('Скажи "Привет!"'); 	    } 	    break;         case '...':        	    // И так далее     } } 

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

В одном из ботов, с которым нам довелось поработать на поздней стадии, ядром функционала была выросшая из двух if-ов простыня на 4000 строк и ~70 условий только верхнего уровня — с проверкой того, что на душу легло — иногда путей, иногда команд, иногда путей и команд. Все эти условия проверялись на каждое действие пользователя и обращались к вспомогательным функциям из соседнего объекта-простыни, также выросшего из нескольких строк. Надо ли говорить, сколь медленно и вразвалочку шел этот проект?

Hobot Framework

Начиная ActualizeBot мы уже представляли, каким большим он будет, и нашей первой задачей было сохранить, расширяемость и скорость разработки.

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

Все это в расчете на большие проекты было написано на TypeScript и получило элегантное название Hobot, намекающее, конечно, на навигационные пайплайны.

Контроллер это простой объект из трех свойств:

  • path — строковый идентификатор пути, используемый для инициализации и навигации по инициализированным путям
  • get — метод, который выполняется, когда мы отправляем пользователя из другого контроллера с помощью метода hobot.gotoPath(ctx, path, data?). В него всегда отправляется контекст пользователя для работы с телеграмом и опционально — одноразовый объект data с кастомной информацией, которая может быть нужна вам в этом контроллере для логики или рендера
  • post — тоже метод. Он выполняется всегда, когда пользователь отправляет сообщение или апдейт, находясь на данном пути. Всегда принимает контекст и updateType — один из типов апдейтов, которые может прислать пользователь: text, callback_query и так далее. updateType выдергивается из уже имеющегося ctx для лаконичности проверок действий пользователя

Пример контроллера:

const anotherController = {     path: 'firstPath',     get: async (ctx, data) =>          await ctx.reply('Welcome to this path! Say "Hi"'),     post: async (ctx, updateType) => {         // Проверяем тип и содержание апдейта: text / callback_query / etc...         if (updateType === updateTypes.text && ctx.update.message.text === 'Hi') {             await ctx.reply("Thank you!");             // hobot байндится в методы контроллеров в составе this:             this.hobot.gotoPath(ctx, 'secondPath', { userJustSaid: "Hi" });         } else {             // Не уходим с этого пути, продолжаем ждать от пользователя того что надо             await ctx.reply('We expect "Hi" text message here');         }     } } 

Выглядит чуть сложнее чем в начале, но зато сложность останется такой же, когда у вас будет 100 или 200 путей.

Внутренняя логика тривиальна и удивительно, что никто этого еще не сделал:
Эти контроллеры складываются в объект, ключами которого являются значения свойства path и вызываются из него по этим ключам при действиях пользователя или при навигации с помощью hobot.gotoPath(ctx, path, data?).

Навигация выделена в отдельный метод с целью не касаться переменной пути и логики навигации, а думать только о бизнес-логике, хотя всегда можно руками поменять ctx.session.path, что, конечно, не рекомендуется.

Все, что нужно сделать, чтобы ваш новый бот с нерушимой структурой заработал — запустить обычный telegraf-бот и передать его и объект конфига в конструктор Hobot. Объект конфига состоит из контроллеров, которые вы хотите инициализировать, дефолтного пути и пар команда / контроллер.

// Инициализируем обычный telegraf-бот const bot = new Telegraf('ВАШ_ТОКЕН');  // Инициализируем фреймворк export const hobot = new Hobot(bot, {     defaultPath: 'firstPath',     commands: [         // Список команд, по вводу которых будет выполняться         // get контроллера с данным путем:         { command: 'start', path: 'firstPath' }     ],     controllers: [         // Объекты-контроллеры, импортированные из своих файлов         startController,         nextController     ] });  // Cтартуем telegraf-бот, с которым мы можем продолжать свободно взаимодействовать bot.launch(); 

В завершение

Неявные плюсы разделения простыни на контроллеры:

  • Возможность поместить в отдельные файлы рядом с контроллерами изолированные функции, методы и интерфейсы, замыкающиеся на логику данного контроллера
  • Значительное снижение риска случайно сломать все
  • Модульность: включить / выключить / дать определенному сегменту аудитории ту или иную логику можно просто внося и удаляя из массива контроллеры, в том числе, апдейтом конфига без программирования — для этого, конечно, надо написать пару букв, так как мы до этих нужд еще не дошли
  • Возможность внятно говорить пользователю, что именно от него ожидается, когда он (что бывает часто) делает что-то не так — и делать это там, где этому самое место — в конце обработки метода post

В наших планах:

В этой статье рассмотрен базовый сценарий работы Hobot с текстовыми сообщениями.
Если тема окажется актуальной, мы поделимся другими техническими тонкостями фреймворка и нашими наблюдениями из практики разработки и использования телеграм-ботов в дальнейших статьях.

Ссылки:

Установка бота выглядит так: npm i -s hobot
Репозиторий с walkthrough в README.MD и песочница
Готовый бот, работающий в продакшене на базе Hobot.

Спасибо за внимание, буду рад услышать ваши вопросы, пожелания или идеи для новых ботов!

ссылка на оригинал статьи https://habr.com/ru/company/actualizebot/blog/489008/

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *