Скорее всего, вы слышали о «12 стартапах за 12 месяцев» или о продуктах, которые родились на хакатонах. Такие истории всегда вдохновляли меня, поэтому я придумал свой челлендж: я сделал простой сервис за выходные.

Проблема

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

Решение

Обзор ботов в одном месте - теперь думаю что гораздо раньше нужно было это сделать
Обзор ботов в одном месте - теперь думаю что гораздо раньше нужно было это сделать

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

После этого я нашел, где появляются первые ценные продуктовые функции, и остановился на этой точке.

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

Вкратце дорожная карта выглядела так:

  1. Middleman-proxy между API Telegram и бэкендом чат-бота.

  2. Перехват данных и их сохранение.

  3. Каркас панели управления и вход с помощью Telegram.

  4. Добавление/редактирование ботов через панель управления.

  5. Просмотр собранных данных: пользователи бота, группы и сообщения <- на самом деле это и есть продукт, все остальное - необходимо для его работы.

Подход

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

Я также старался сделать все как можно проще для максимального ускорения:

  • Я использовал только хорошо знакомые мне технологии: Telegram API, Vue.js+Quasar, node.js+fastify, TypeORM+Postgres, Cloudflare, Gitlab CI/CD+Docker и набор библиотек вроде Moment.js, Nanoid и html-sanitize.

  • TypeScript как на фронте, так и на бэке, чтобы избежать переключения между синтаксисами, программируя full-stack.

  • Простой REST API (за исключением Telegram API для middleman).

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

  • Я использовал базовый material design с небольшой кастомизацией элементов (я потратил на это меньше половины часа).

  • Mobile-first — никакой дополнительной работы для поддержки и мобилки, и десктопа.

Я все же потратил время на безопасность, чтобы спокойно сделать сервис публичным. Суммарно это заняло у меня около 1/3 времени.

Технические детали

Остановлюсь на моментах которые могут быть более интересными или менее очевидными, а рассказ о том как я писал REST-админку, красил Quasar и складывал перехваченные запросы/ответы от Телеграма в базу пропущу =).

Схема работы

Идея простая: прокидывать запросы между Телеграмом и ботом туда и обратно. Для этого нужно поменять хук через API телеграма через setWebhook- это делается через админку. Это позволит перехватывать и логировать сообщения от пользователя. Дальше - поменять apiRoot в самом боте. Такую настройку поддерживает популярный telegraph для node.js, да и большенство библиотек работы с Telegram API.

Работают только webhook-и, без long-polling. В целом, на моей практике long-polling это больше для отладки, а боевые работаеют через хуки.

Ботсман встраивается между Телеграмом и бэком бота и работает как прокси
Ботсман встраивается между Телеграмом и бэком бота и работает как прокси

Вход при помощи Телеграма

убийца конверси - проверено
убийца конверси - проверено

Из предыдущего опыта знаю, что пользователи пугаются стандартной формы входа в Телеграм из-за того что она запрашивает телефон. Поэтому вход сделан кастомный через бота, зарегистрированного через BotFather.

Процесс такой: при каждой загрузке страницы с кнопкой входа пользователь полачает уникальный токен, который сохраняется на бэке и передаётся с командой /start через deeplink: https://t.me/${botName}?start=${startDataString}
Дальше бот спрашивает подтверждение, по которому код привязывается к аккаунту Телеграм. Тем временем, фронт шлёт запросы на бэк, проверяя логин пользователя по этому коду. Можно было бы сделать вебсокеты, но это тоже было отложено для ускорения.

Вход через своего бота даёт дополнителые плюшки:

  • Можно использовать как канал push-уведомлений. Прямо сейчас их нет, но мне самому были бы удобны уведомления о событиях, начиная с алертов что бэк бота упал, и продолжая кастомиными событиями - например, что пользователь вызвал определённую команду. В свои боты планирую добавить команду "/feedback" и перенаправлять сообщения себе.

  • WebView - можно не ходить в web, а вызывать админку прямо из бота. Оказалось супер удобно - с телефона захожу проверять свой выводок ботов именно так.

  • Я замкнул вход в сервис на собственный API, и трекаю заходы так же как и остальных ботов.

Разделяемый монолит

Это важный элемент ускорения - не тратить время на развёртывание нескольких сервисов/микросервисов, связывание их по API/RPC, добавление pub-sub через RabbitMQ или Kafka, настройку autoscale в AWS. Ну, вы поняли.

Поэтому для первой версии я всегда выбираю монолит на бэке - даже для более крупных проектов. Исключение - если нагрузку будут давать с первых дней, что бывает только при расширении уже high-load проектов. Но это другая история.

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

  • Выделение сервис-классов и вынос их в сервис-контейнер уровня проложения.

  • Добавление шины событий (я использую mitt) - её будет легко расцепить на уровне событий, не трогая логику и сервисы.

  • Разбивка на engines - это линии разреза будущих сервисов. Engine - небольшое приложение, включающие свой сервис-контейнер и сервис-классы, модели, и иногда - шину событий и внешний API. Engine обычно зависит от внешнего контейнера, например для доступа к БД и core-сервисов, которые станут микросервисами когда-нибудь.

  • Для удобной будущей распилки базы имеет смысл не связывать разные engine внешними ключами. В этом случае все сообщения от Телеграм просто падают в таблицу и будет легко переехать на MongoDB, DynamoDB или другой NoSQL.

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

Кстати, интересный аргумент в пользу монолитов на старте встретил в статье на Хабре: https://habr.com/ru/post/701796/ - здесь имплементацию SOA автор начал с монолита, иначе было бы просто не запустить, а скорость в построении упала для SOA в 5 раз. Очень похоже на реалии разработки. В целом, в Factorio разделяемый монолит тоже работает: строится центральная база, по возможности оставляется место для масштабирования производства, а дальше - подключение вынесенных отдельных микро-баз по мере необходимости.

Безопасность

Кроме стандартного firewall всегда есть желание спрятать IP сервера совсем - для этого есть Cloudflare. Но есть ещё исходящие запросы - webhook к ботам. Чтобы спрятать сервер в этом случае можно использовать схему proxy - например, развернуть несколько VDS которые не будет делать ничего кроме прокидывания запросов - наружу будут торчать только эти URL (которые стоит дополнительно защитить). Я поступил ещё проще и сделал proxy через Cloudflare Workers и все запросы уходят с IP Cloudflare. Так же можно сделать через AWS Lambda или другие serverless-сервисы.

Мотивация

Поскольку я работаю над важным майлстоуном для Clubeeo, который включает в себя low-code/no-code, каркас интеграций и встроенных приложений, я почувствовал, что мне нужно как очистить голову. Можно было бы конечно провести время в Factorio или попробовать вышедший недавно Dwarf Fortress с графикой. Но всё-таки это приятное чувство завершённой работы ничем не заменить. Поэтому мысль о создании и развертывании чего-то небольшого и полезного, на чем я мог бы сосредоточиться на пару дней, показалась классным приключением. Так оно и оказалось =)

И последнее, но не менее важное: я изначально планировал статьи и пока кодил думал о том как поделюсь с вами этой историей.

Спасибо что её прочитали!

Если есть вопросы - пишите в комментариях, буду рад на них ответить.

Комментарии (2)


  1. Artem652462
    15.12.2022 10:07
    +1

    А источников не будет? Хотел посмотреть на архитектуру


    1. urvalla Автор
      16.12.2022 08:22

      Да, мысль хорошая - подготовлю и выложу исходники.