ITS Reg - сервис для LowCode/NoCode создания телеграм-ботов на основе конечных автоматов.
cp .env.example .envДалее заполнить .env файл.
Для запуска полностью в docker-compose:
docker compose --env-file .env up -dДля запуска вне докера:
DATABASE_URI='' PORT=8500 JWT_SECRET=s3cr3t go run cmd/http/main.goгде DATABASE_URI - строка подключения к Postgres базе данных;
PORT - порт, который будет прослушиваться сервисом;
JWT_SECRET - ключ шифрования JWT-токена.
На данный момент сервис не имеет клиента. Создание и управление ботами на платформе осуществляется через HTTP-запросы. OpenAPI спецификация описана в [api/openapi/bots.yaml]. Запросы к серверу должны содержать JWT-токен:
GET localhost:8500/api/v2/bots
Authorization: Bearer <place-your-jwt-token here>Упрощённая схема взаимодействия компонентов бота:
--------------------------- ---------------------
| | | | ------------------------
| telegram.InstanceManager | | command.Process --> | telegram.MessageSender |
| | | | ------------------------
| ----------------- | | ----------------- | -----------------------------
| | Bot #1 instance | - | | | bots.Partcipant | --> | ports.ParticipantRepository |
| ----------------- \ | | ----------------- | -----------|-----------------
| \ | | ----------------- | ---------- V --------
| ----------------- \ | | bots.Bot | | | postgres.Repository |
| | Bot #2 instance | ------ >| | ----------- | | ---------- ^ --------
| ----------------- / | | | bots.Node | | | -----------|-----------
| / | | | ----------- | --> | ports.Bots.Repository |
| ----------------- / | | | ----------- | | -----------------------
| | Bot #2 instance | - | | | | bots.Node | | |
| ----------------- | | | ----------- | |
| | | ----------------- |
--------------------------- ---------------------
Бот (bots.Bot) есть реализация конечного автомата.
Конечный автомат (далее - КА) bots.Script состоит из:
- узлов (
bots.Node); - точек входа (
bots.Entry).
Узел - минимальная структурная единица бота.
Каждый узел характеризуется уникальным номером в рамках бота - состояние (state).
Состояние есть положительное целое число.
Узел состоит из:
- состояния (
Node.state) - названия узла (
Node.title); - массива сообщений (
bots.Message), отправляемых пользователю; - упорядоченного списка исходящих рёбер (
bots.Edge); - опций (кнопок) ответа пользователем (
bots.Option).
Точка входа есть именнованное состояние, с которого начинается прохождение сценария пользователем.
По умолчанию используется точка входа start - при использовании пользователя команды /start.
Рассмотрим на примере.
------------
красная | Реальность |
----- * ----------- / ------------ -------------------
| Имя | --> | Таблетка? | +-----------------| Некорректный ввод |
----- ----------- \ ----- * -------------------
^ синяя | Сон | |
| ----- |
| |
---------------------------------
Необходимо получить от пользователя его имя и выбор таблетки: красной или синей.
Очевидно, что каждому вводу будет соответствовать своё состояние бота; а следовательно, и узел.
Обозначим их как state=0 и state=1.
А также информационные сообщения в случае различных выборов пользователя в последнем вопросе: 3, 4, 5.
Ввод имени будет представлен узлом:
{
"state": 1,
"title": "Имя",
"messages": [ { "text": "Введите Имя" } ]
}Далее необходимо описать упорядоченное множество исходящих рёбер из узла.
Так как при любом вводе пользователя будет совершён переход к новому состоянию, используется предикат type=always.
Следующее состояние to=2.
Операция, совершаемая при переходе по ребру - единократное сохранение ответа в БД, то есть operation=save.
Опции в данном узле опущены, так как не подразумевается использование пользователем кнопок (кроме кнопок клавиатуры, естественно).
Получаем:
{
"state": 1,
"title": "Имя",
"messages": [ { "text": "Введите Имя" } ],
"edges": [
{
"predicate": { "type": "always" },
"to": 2,
"operation": "save"
}
]
}Следующий узел имеет несколько возможных вариантов ответа.
Используется связка predicate.type=exact и options.
{
"edges": [
{
"predicate": {
"type": "exact",
"text": "Красная"
},
"to": 4,
"operation": "save"
},
{
"predicate": {
"type": "exact",
"text": "Синяя"
},
"to": 5,
"operation": "save"
}
],
"options": [
"Красная",
"Синяя"
]
}Пользователь при наличии кнопок всё ещё может ввести свой текст в ответ.
Данная ситуация иногда требует собственной обработки с использованием предиката always с наименьшим приоритетом:
{
"edges": [
{
"predicate": {
"type": "exact",
"text": "Красная"
},
"to": 4,
"operation": "save"
},
{
"predicate": {
"type": "exact",
"text": "Синяя"
},
"to": 5,
"operation": "save"
},
{
"predicate": { "type": "always" },
"to": 3,
"operation": "noop"
}
]
}Стоит обратить внимание на operation=noop.
В данном случае невалидный ввод не имеет смысла сохранять в БД, поэтому используется операция-заглушка: noop.
Существуют ситуации, когда ввод, отличающийся от предложенных опций (кнопок) стоит сохранять.
Например, когда пользователь должен выбрать из N вариантов или ввести свой вариант ответа.
В последнем случае будет использована operation=save.
Другие, нерассмотренные в примере особенности:
- состояние
0зарезервировано как конец выполнения КА и не может являться состоянием бота. - за один узел может быть отправлено несколько сообщений подряд.
operation=saveиoperation=appendимеют различную семантику при прохождении данного узла несколько раз:saveперезаписывает существующий ответ, аappendдобавляет в конец через сепаратор\n.appendможет использоваться для вопросов с множественным выбором ответов.
Запрос:
GET http://{{server}}/api/v2/bots/{{id}}/answersвозвращает CSV-таблицу ответов в следующем виде:
| # | Отметка времени | Узел #1 | ... | Узел #N |
|---|---|---|---|---|
| abcdefg | 2025-12-31 23:59 | Ответ 1 | ... | Ответ N |
| ... | ... | ... | ... | ... |
| abcd345 | 2025-12-31 23:59 | Ответ 1 | ... | ... |
Первый столбец обозначает thread_id - уникальный идентификатор каждого прохождения пользователем скрипта бота,
начиная от любого entry.
Проще говоря: каждая последовательность ответов от пользователя, начиная от команды /start.
Отметка времени есть начало прохождения скрипта начиная от точки входа.
Далее перечисляются узлы и ответы на них в последовательности увеличения state.
Будут перечислены только те узлы, в которых существует хотя бы один ответ.
Название столбца совпадает с Node.title.
Сервис допускает использование совместно с электронными онлайн-таблицами. Для этого необходимо в свободный лист таблицы вписать формулу:
=IMPORTDATA("http://{{server}}/api/v2/bots/{{id}}/answers?jwtToken={{place-jwt-token-here}}")
Через несколько секунд будет автоматически скачана и вставлена нередактируемая автоматически обновляемая таблица.
Чтобы насильно обновить данные в такой таблице, иногда требуется вставить формулу заново. Или написать расширение для электронных таблиц, чтобы сделать кнопку, которая делает эту рутину за Вас :).