04. Low-Code разработка

Подробное руководство по платформе Metabot

Кастомные таблицы

«После того, как бизнес-данные были централизованы и интегрированы, ценность базы данных больше, чем сумма ранее существовавших частей».
— Ларри Эллисон, основатель Oracle


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

Кастомные таблицы от Metabot — это альтернатива (или дополнение) к таким продуктам как Google Sheets, AirTable и Notion, созданное специально для разработки приложений с бизнес-логикой.

Примеры использования:

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

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

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

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

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

Работа с SQL доступна на выделенном сервере и в on-premise коробочной версии.

Конструктор таблиц 

Конструктор таблиц находится в верхнем меню бота в разделе Таблицы:

Снимок экрана 2023-02-14 122655.png

Он предназначен для работы с таблицами, например, для создания таблиц, их форматирования и удаления.

Для создания таблицы необходимо нажать кнопку Создать:

Снимок экрана 2023-02-14 123312.png

В открывшемся окне Создание таблицы можно заполнить информацию о создаваемой таблице:

image.png

Опция Синхронизация структуры доступна только администраторам платформы.

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

После создания таблицы появляются функции ее редактирования, удаления, создания полей и их обновления в БД:

image.png

Конструктор полей

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

Снимок экрана 2023-02-17 140114.png

Первым полем любой таблицы всегда является ID строки. Это поле создается автоматически при создании таблицы и не подлежит удалению. По этому полю вы сможете идентифицировать отдельные строки таблицы, а также искать и редактировать записи через JS.

Остальные необходимые поля можно добавить щелкнув по кнопке Создать:

Снимок экрана 2023-02-14 125805.png

В открывшемся окне Создание поля необходимо заполнить информацию о создаваемом поле:

image.png

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

Снимок экрана 2023-02-17 140408.png

После создания и настройки всех ячеек таблицы можно провести обновление таблицы в базе данных платформы. Для это необходимо в конструкторе таблиц нажать Обновить все поля в БД:

image.png

Это синхронизирует и обновит таблицу в базе данных, а так же сгенерирует ее JSON словарь (Структуру таблицы).

На текущий момент, в качестве базы данных платформа Metabot поддерживает PostgreSQL.

Формирование модели данных через текстовое описание мета-словаря на базе JSON в текущей версии не доступно. 

Просмотр и редактирование данных

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

image.png

Снимок экрана 2023-02-14 132358.png

После этого нажмите на кнопку Создать и добавьте необходимые данные.

кт.png

image.png

После создания запись появится в таблице. Записи так же можно редактировать и удалять.

По итогу у вас получится таблица, которая может выглядеть, например, так:

image.png

Данная таблица "Регионы" будет использоваться для примеров использования методов в Cправочнике функций JS.

Фильтры

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

При задании фильтров и нажатии на кнопку Применить фильтр будут выведены соответствующие записи в таблице:

image.png

С кастомными таблицами так же можно работать с помощью JS методов. Все методы и их описание вы можете посмотреть в Справочнике по функциям JS

Блокировки как средство управления параллелизмом

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

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

Состояние гонки (Race Condition)
Взаимная блокировка (Deadlock)
Атомарная операция (Atomic Operation)
Параллельные вычисления
Распределённый менеджер блокировок

Почему блокировки важны для вашего бизнеса

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

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

Приложение Metabot предоставляет различные функции для работы с блокировками в контексте вашего бизнеса или конкретного бота. Например, функции получения блокировки включают в себя проверку блокировки и захват блокировки. Эти две операции является атомарными, т.е. выполняются одномоментно, если блокировка доступна для захвата (т.е. если никакой другой процесс не захватил блокировку на момент проверки). Такая атомарная операция автоматически исключает возникновение состояния гонки (Race Condition).

Чтобы не возникло взаимной блокировки, предусмотрено указание времени жизни блокировки (time to leave) в параметрах функций захвата блокировки (см параметр ttlSec). 

Также, для упрощения работы с блокировками, в функции захвата встроены алгоритмы, позволяющие автоматически выполнять повторную попытку захвата блокировки на протяжении указанного промежутка времени (см параметр maxWaitSec). 

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

ВНИМАНИЕ!!! Будьте очень аккуратны при использовании функций блокировок, неправильное использование может нарушить работоспособность всей системы! Ваш бот может быть внесен в черный список а все вебхуки и задания бота будут исключаться из очередей!

Подробное описание назначения блокировок с описанием примеров смотрите в статье про блокировки в нашем блоге. ДОБАВИТЬ ССЫЛКУ!

Методы блокировок в Metabot

Приложение Metabot предлагает ряд методов, которые помогают управлять блокировками.

Все перечисленные ниже методы вам не нужны, если вы используете стандартные функции связанные с контакт-центром: lead.assignDialogToNextOperator, lead.assignDialogToOperator или lead.getNextOperators и т.д., так как данные функции уже включают в себя алгоритмы блокировок.

Методы генерации имени блокировок

Использование на данный момент смысла не имеет, функции блокировок сами добавляют дополнительные префиксы к именами блокировок указанным в аргументах функций.

Метод Описание
bot.getLockNameForBot(string $lockName, string $lockPrefix = ''): string Генерирует имя блокировки по боту
bot.getLockNameForBusiness(string $lockName, string $lockPrefix = ''): string Генерирует имя блокировки по бизнесу

Методы перехвата блокировок

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

Метод Описание
bot.waitForBusinessLock(string $lockName, string $lockPrefix = '', ?int $ttlSec = null, $maxWaitSec = 300): bool

Захватывает блокировку по бизнесу

bot.waitForBotLock(string $lockName, string $lockPrefix = '', ?int $ttlSec = null, $maxWaitSec = 300): bool

Захватывает блокировку по боту

Если нет понимания что указать в параметре ttlSec, укажите значение равное 30 (это значит что блокировка будет автоматически удалена спустя 30 секунд, если вы забыли это сделать в коде), это необходимо, чтобы не возникло взаимной блокировки, если же вы указываете время жизни равное 0, то блокировка не ограничена по времени, но в таком случае нужно подходить к алгоритмам еще тщательнее, чтобы не забывать освобождать блокировку.

Методы удаления блокировок

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

Метод Описание
bot.releaseAllCurrentLocks(): bool Освобождает все блокировки захваченные в текущем скрипте
bot.releaseCurrentLockForBusiness(string $lockName, string $lockPrefix = ''): bool Освобождает блокировку захваченную текущем скрипте по бизнесу
bot.releaseCurrentLockForBot(string $lockName, string $lockPrefix = ''): bool Освобождает блокировку захваченную в текущем скрипте по боту
bot.releaseLockForBusiness(string $lockName, string $lockPrefix = ''): bool Освобождает блокировку по бизнесу
bot.releaseLockForBot(string $lockName, string $lockPrefix = ''): bool Освобождает блокировку по боту

Методы проверки наличия блокировок

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

Метод Описание
bot.hasLockForBusiness(string $lockName, string $lockPrefix = '', $checkValue = null): bool Проверяет существование блокировки по бизнесу
bot.hasLockForBot(string $lockName, string $lockPrefix = '', $checkValue = null): bool Проверяет существование блокировки по боту

Все эти методы обеспечивают гибкость при работе с блокировками в вашем приложении Metabot. С их помощью вы можете создавать более надежные и эффективные бизнес-процессы.

Подробнее методы перечисленные выше и примеры к ним вы можете изучить в Справочнике по функциям JS

ВНИМАНИЕ!!! Если у вас нет понимания как пользоваться функциями блокировок, то лучше не экспериментировать и обратиться к нашим специалистам!

Интеграция с ChatGPT

Сейчас во многие сферы нашей жизни все больше проникает искусственный интеллект. Так, ChatGPT, разработанный OpenAI, является универсальным инструментом, который приносит пользу во многих областях и широко распространен.

Чтобы не отставать от всего мира, мы приняли решение о внедрении возможности работы с ChatGPT на нашей платформе! Это позволит нам расширить ее функциональность и предоставить пользователям доступ к бесконечным знаниям и ресурсам, которые дает этот искусственный интеллект.

На нашей платформе работа с ChatGPT осуществляется при помощи разработанных командой Metabot плагинов.

Плагин для GPT можно найти в Общих плагинах на платформе. Он содержит три скрипта:

Рекомендуем ознакомиться с документацией Плагины

Как использовать GPT плагин

Есть два способа взаимодействия с GPT в боте:

Первый вариант подходит для случаев, когда GPT используется в боте точечно в определённых скриптах. Этот вариант является предпочтительным и с более управляемой логикой под каждый скрипт. Логика такого скрипта является более понятной и "прямой".

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

Желательно использовать GPT не во всех скриптах бота, а только в определённых, чтобы не загружать GPT большим количеством запросов и не увеличивать время его ответа.

Рассмотрим использование обоих вариантов на примере.

Скрипт "Общаться только с GPT"

В скрипте размещаем две команды:

Далее создаем меню, которое будет автоматически добавляться к GPT ответам.

В первой команде перед запуском Callback подключаем и инициализируем GPT (1-2 строка), чистим текущую сессию (4 строка) и запоминаем контекст с кодом текущего скрипта (gpt.setContext(bot.getCurrentScriptCode()) / gpt.setContext(null) / gpt.disableByContext()). Контекст можно использовать, например, для поиска инструкции Prompt для GPT, или для выключения GPT в Fallback с помощью gpt.disableByContext().

const Gpt = require('Common.GPT.GPT')
const gpt = new Gpt()
gpt.setContext(null)
gpt.clearSession()

В JavaScript Callback получаем меню из текущего скрипта (2 строка), и добавляем команды для его вывода (5-11 строки). Если пользователь нажал на кнопку, происходит выход из Callback, с помощью планировщика.

Есть два варианта запуска другого скрипта при нажатии на кнопку выхода из меню пользователем:

bot.scheduleScript(runScriptId, leadId)
throw new Error('stop') // ИЛИ ДОБАВИТЬ КОМАНДУ СТОП, ПОСЛЕ ДАННОЙ КОМАНДЫ JS-CALLBACK
bot.run({
    "script_id": runScriptId,
    //"script_code": "your_script_code",
    //"skip_till_command_id": 111,
    //"stop_current_flow": false,
  })
  
// ДАННАЯ ТОЧКА КОДА ДОЛЖНЫ ВЫПОЛНЯТЬСЯ, ТК СКРИПТ ПРЕРВАН ВЫШЕ С ПОМОЩЬЮ МОМЕНТАЛЬНОГО ВЫПОЛНЕНИЯ СКРИПТА (bot.runJob)
bot.sendMessage('ОШИБКА 1! Данная точка кода никогда не должна выполняться!') 
  
return true

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

// Получаем меню из текущего скрипта
let menuButtons = bot.getButtonsForCurrentScript();

// Запустится сразу при выполнении данной команды
if (isFirstImmediateCall) {
  bot.sendMessage(
    'Для выхода из данного режима нажмите кнопку "Назад" или напишите "меню" или "бот", подробнее смотрите в инструкции - Как пользоваться ботом', 
    menuButtons
  )
  return false
}

let runScriptId = bot.getScriptIdFromButtonsByIncomingMessage(menuButtons)
// Если нажали на кнопку, выходим из колбэка, с помощью планировщика

if (runScriptId !== null) { 
  // Запуск другого скрипта из текущей точки
  bot.run({
    "script_id": runScriptId
  })
  
  // ДАННАЯ ТОЧКА КОДА ДОЛЖНА ВЫПОЛНЯТЬСЯ, ТАК КАК СКРИПТ ПРЕРВАН ВЫШЕ С ПОМОЩЬЮ МОМЕНТАЛЬНОГО ВЫПОЛНЕНИЯ СКРИПТА (bot.runJob)
  bot.sendMessage('ОШИБКА 1! Данная точка кода никогда не должна выполняться!') 
  
  return true
}
// Код ниже запустится, когда лид ответит находясь в текущем скрипте

// Подключение и инициализация GPT
const Gpt = require('Common.GPT.GPT')
const gpt = new Gpt()

// Взять входящее сообщение из вебхука, отправить в GPT и вывести ответ
gpt.runFromWebhook(null, menuButtons)

return false // не выходим из JS Callback

Если же пользователь ответил оставшись в текущем скрипте, то выполняется подключение и инициализация GPT (31-32 строки), а затем входящее сообщение отправляется GPT из вебхука и его ответ выводится пользователю (35 строка).

Для Telegram: ответное сообщение будет обновляться по мере получения ответов от GPT. Если длина ответа от GPT превышает установленный maxMessageLength лимит, то ответ будет отправлен несколькими сообщениями.

После отправки сообщения GPT прикрепит к нему кнопки.

Системный скрипт "Fallback"

В данном скрипте взаимодействие с GPT выполняется при помощи команды Выполнить JavaScript. JS скрипт проверит контекст и не будет запускать GPT, если он выключен для данного раздела. Если GPT включен, то скрипт запустит его и выведет ответ с прикрепленным к нему последним меню.

Также в Fallback происходит проверка на существования ответа от GPT, и если GPT ответа нет или GPT не запускался в данном разделе, то будет выведен обычный Fallback текст. Для этого в Fallback после команды Выполнить JavaScript следует добавить команду Отправить текст с условием: return !(memory.getAttr('has_gpt_answer') * 1).

Первым делом в скрипте выполняется подключение и инициализация GPT (2-3 строки). Затем выполняется проверка на включение GPT в разделе из которого был совершен переход в скрипт Fallback (6 строка).

// Подключение и инициализация GPT
const Gpt = require('Common.GPT.GPT')
const gpt = new Gpt()

// Если GPT выключен в данном разделе
if (gpt.isDisabledByContext()) {
  return
}

// Поиск шаблона промта в кастомной таблице по текущему контексту (коду скрипта)
let gptContext = gpt.getContext()
if (gptContext !== null && typeof gptContext === 'string' && gptContext.length > 0) {
  let items = table.find('gpt_prompts', ['prompt'], [['context', '=', gptContext]])  
  if (items.length > 0) {
    let item = items[0]
    if (item.prompt !== null && item.prompt.length > 0) {
      let startPrompt = 
      `GPT, сейчас ты в роли чат-бота и являешься консультантом в ИТ-компании, отвечай так, будто ты работаешь в этой компании.
      Если ответа в этом тексте нет, то ответь Клиенту: "Извините, но у меня не хватает знаний для ответа на вопрос".  
      Можешь дополнять ответ смайликами (emoji).
      `  
      
      if (gptContext === 'our_products_and_services') {
        startPrompt += ` Ответ клиенту обязательно дополни следующей фразой:
         "Чтобы я смог лучше понять, что именно вы хотите, перейдите, пожалуйста, в соответствующий раздел продуктов или услуг на нашем сайте."
        ` 
      }
      
      if (gptContext === 'our_products' || gptContext === 'our_services') {
        startPrompt += `Обязательно строй ответы так, чтобы пытаться продать товар.`
      }
      
      gpt.gptSession.addMessage('system_start', startPrompt)
      gpt.gptSession.addMessage('system', item.prompt)
    }
  }
}

// Взять входящее сообщение из вебхука, отправить в GPT и вывести ответ
gpt.runFromWebhook()

Затем происходит поиск шаблона prompt в кастомной таблице по текущему коду скрипта. Если проверка прошла успешно, то GPT отправляются указания для построения ответа в зависимости от контекста (17-31 строки). Последним этапом входящее сообщение отправляется GPT из вебхука и его ответ выводится пользователю (40 строка).

База знаний и GPT

Существует еще один вариант использования GPT в боте. Для него понадобится создать свою базу знаний, с которой будет работать GPT.

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

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

Для реализации базы знаний потребуется создать таблицу gpt_knowledge_base и скрипт, состоящий из двух команд: Выполнить JavaScript и Выполнить JavaScript Callback и пунктов меню, которые крепятся к ответу.

Таблица должна содержать следующие поля:

Поле Тип Описание
id AUTOINCREMENT ID статьи

context

TEXT Контекст статьи

content

TEXTAREA Содержимое статьи

embeddings

VECTOR Эмбеддинг-вектор текста статьи

Далее необходимо добавить в таблицу необходимые "статьи" и заполнить поле embeddings, например, при помощи следующего кода:

let tmp = kb.getRows(null, 1)
let article = tmp.length > 0 ? tmp[0] : null
if (article !== null) {
  let embeddings = gpt.gptDriver.getEmbeddings(article['content'])
  kb.saveEmbeddings(article['id'], embeddings)
}
return false

В команде Выполнить JavaScript в скрипте "База знаний" подключаем и инициализируем GPT (1-2 строка), чистим текущую сессию (4 строка) и запоминаем контекст с кодом текущего скрипта (gpt.setContext(bot.getCurrentScriptCode()) / gpt.setContext(null) / gpt.disableByContext()).

const Gpt = require('Common.GPT.GPT')
const gpt = new Gpt()
gpt.setContext(null)
gpt.clearSession()

В команде Выполнить JavaScript Callback повторно подключаем и инициализируем GPT. 

// Подключенире и инициализация GPT
const Gpt = require('Common.GPT.GPT')
const gpt = new Gpt()

const callbackStartMessage = 'Для выхода из данного режима нажмите кнопку "Назад" или напишите "меню" или "бот", подробнее смотрите в инструкции — Как пользоваться ботом'
const kbContext = null // контекст поиска статей
const kbLimit = 1 // количество статей передаваемых в prompt
const kbMaxDistance = 0.17 // для отсечки найденных статей по косинусному расстоянию

let prompt = `GPT, сейчас ты в роли чат-бота и являешься консультантом в ИТ-компании, отвечай так, будто ты работаешь в этой компании.
Ответ на вопрос ищи ТОЛЬКО среди информации, которая тебе отправлена, ничего не придумывая и не ищя ответ в своих общих знаниях.
Добавляй emoji в предложения с ответом, там где это уместно.
Если ответ не возможно сформировать на основе предоставленной информации, то ответь Клиенту: "Извините, но у меня не хватает знаний для ответа на вопрос"`

return gpt.runForKnowledgeBase(callbackStartMessage, prompt, kbContext, kbLimit, kbMaxDistance)

Затем отправляем запрос к GPT с сформированным заранее prompt (15 строка).

Metabot API

Metabot API

Конструктор API

«API — это продукт. API — это также то, как вы разрабатываете, продаете и доставляете цифровые и все чаще физические продукты своим клиентам»
— Кин Лейн, главный евангелист Postman


Коммуникационная low-code платформа Metabot обладает встроенным конструктором API для организации взаимодействия с внешними системами по REST в соответствии со стандартом OpenAPI 3.

У любого чат-бота, создаваемого на Metabot, есть внутреннее API — для обращений внешних систем к чат-боту, и внешнее API — для обращений чат-бота к внешним системам.

API состоит из точек интеграции, называемых эндпоинтами (endpoints) или конечными точками (точками доступа, точками связи и т.п.). Настройка точек интеграции происходит в разделах Внутреннее и Внешнее API доступное из меню Настроек бота.

Снимок экрана 2024-02-05 123004.png

Внутреннее API представляет из себя конструктор API для вашего чат-бота. Здесь вы можете создать и кастомизировать ваши точки доступа к чат-боту, задать алиас (alias) точки и возвращаемый ответ с помощью JavaScript. В JavaScript можно планировать другие задания в очереди работ чат-бота (асинхронные), а также вызывать внешнее API (колбэк/callback).

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

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

Удобство синхронного API в том, что вы моментально получаете ответ на запрос, нет необходимости отдельно опрашивать результат выполнения задания или устанавливать отдельный колбэк. При этом, в JavaScript, являющимся телом API запроса можно также планировать любые другие «вложенные» задания (например триггеры, скрипты конструктора или рассылки), такие задания будут выполнены асинхронно, для них в очереди будут спланированы и выполнены отдельные задания.

Если вы хотите получить ответ от внешнего API позже, в момент выполнения отдельного асинхронного «вложенного» задания, то для этого вам необходимо вызвать внешний API в запущенном скрипте или триггере (с помощью внешнего эндпоинта или JS функции api.callEndpoint). Таким образом, вы можете создать любую цепочку связанных заданий и получить результат идентичный вызову коллбеков в необходимых точках.

Вызвать внешнее API можно с помощью JS функции api.callEndpoint(...), подробности смотрите ниже, в разделе с описанием внешнего API.

Внутреннее API

1.png

Структура конечной точки внутреннего API

Описание полей внутреннего эндпоинта представлена в таблице ниже.

Поле Описание
Флаг активности Если убрать флаг, то при обращению через API к данному endpoint будет возвращена ошибка.
Алиас

Дополнительный уникальный идентификатор. Используется как часть URL-адреса для обращения к конечной точке через API, а также для вызова в JavaScript функциях.


Разрешенные символы: буква, цифра, подчеркивание, дефис, слэш (косая черта)


Шаблон URL-адреса конечной точки:

https://адресплатформы/api/v1/bots/{bot_id}/call/{alias}

 

Для каждого бота создается свое пространство имен для API, что означает Алиас должен быть уникален в пределах бота.

JavaScript для вычисления API ответа (Response Body)

JavaScript для вычисления содержимого API ответа. Данный JS код будет выполнен при обращении к внутреннему API (по алиасу), результат выполнения кода будет являться телом ответа на API запрос.


Это значит, что API ответ вычисляется на основе JavaScript. В JavaScript должен присутствовать ключевой оператор return для возврата значения из JS. Выражение которое указывается после return является ответом (Response Body) на API запрос.


Можно возвращать:

  • Строку (будет сконвертирована в json, если формат валидный);
  • Массив;
  • Объект (в том числе JSON-объект).

Пример:


return {
  "key": "value",
  "lead_name": lead.getData('name'),
  "request_param_name": request.json.name
};

В JS можно выполнять любой код, например обращаться к пользователям, или устанавливать переменные в memory, планировать задания бота (триггеры, рассылки, скрипты), а также вызывать внешнее API c помощью api.callEndpoint или другие методов объекта api: post/get/postJson/getJson/request.

Комментарий

Любой текст. Доступно указание любых HTML-тэгов.

На данный момент доступен вызов кастомного внутреннего API только с методом POST и в формате JSON. Методы GET, PUT, PATCH, DELETE не поддерживаются.

2.png

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

Вызов внутреннего API

Узнать структуру запроса и проверить работу внутреннего API можно c помощью Swagger, доступного по адресу платформы. Если платформа устанавливается в контуре вашей сети, то адрес сервера нужно узнать у вашей ИТ службы.

API публичного сервиса под управлением нашей команды можно посмотреть по ссылке.

Чтобы получить доступ к Swagger к публичному серверу, нужно зарегистрировать аккаунт на платформе по ссылке, затем подтвердить электронную почту и авторизоваться на платформе.

Внимание! При работе в Swagger платформы будут выполняться реальные API запросы, а не эмуляция API.

3.png

Также проверить работу внутреннего API можно c помощью Postman или любого другого софта для тестирования API.

4.png

Параметры запроса

В URL запроса нужно указать идентификатор бота botId на платформе и alias внутреннего эндпоинта в боте.

В теле запроса размещаются данные запроса, например:

{
  "lead_id": 135,
  "ticket_id": 7,
  "trigger_id": 1,
  "trigger_code": "trigger_short_code",
  "script_request_params": {
    "first_param": 7,
    "second_param": {
      "any_key": "any_value"
    }
  }
}

С параметрами тела запроса к внутреннему эндпоинту можно ознакомиться в таблице ниже.

Поле Обязательность заполнения Описание

lead_id

-

В JS будет подготовлен и доступен полноценный объект lead, если у лида есть персона, то также будет доступен объект person.

ticket_id

-

В JS будет подготовлен и доступен полноценный объект ticket.

trigger_id или trigger_code

-

В JS будут доступны переменные triggerId, triggerName.

script_request_params

-

В JS будет подготовлены и доступны полноценные объекты request и last_request.
Авторизация запросов API

Для авторизации запросов чат-бота используется Authorization Bearer Token.

При каждом вызове API добавьте в заголовок авторизации <Access Token>. Параметры заголовков для всех запросов:

authorization: Bearer <Access Token>
Host: https://app.metabot24.com
Генерация токена доступа к API

Чтобы получить API токен, нужно создать API пользователя и сгенерировать токен в в Админ-панели Metabot, для этого:

1. Cоздайте или привяжите нового пользователя в разделе Пользователи бизнеса.

16.png

2. При создании пользователя включаем опцию Пользователь API и указываем имя пользователя. Если необходимо указываем роль пользователя и доступ к какому боту нужно получить по API, или оставляем поля по умолчанию.

17.png

3. Нажмите на ссылку Сгенерировать API-токен.

18.png

4. Скопируйте полученный API-токен и используйте его в ваших запросах.

19.png

5. Можете использовать скопированный API-токен для API на странице Swagger (на странице /api/docs). Токен указывается на форме авторизации, которая открывается нажатием на кнопку Authorize.

20.png

Если вдруг не найдете опцию создания API токена в вашем аккаунте, запросите токен в поддержке.

Внешнее API

5.png

При создании новой конечной точки открывается окно создания со следующими параметрами:

6.png

7.png

Вызов внешнего API из чат-бота

Метод для вызова внешнего API из JavaScript:

api.callEndpoint(
  string $externalApiEndpointCode, 
  string|null $uri = null,
  string|null $method = null, 
  string|null $format = null, 
  array|object|string|null $body = null, 
  array|object|string|null $headers = [],
  array|object|string|null $requestOptions = [], 
  array|object|string|null $httpClientConfig = [], 
  string|null $proxyUrl = null
): array|string|null
Пример №1 вызова внешнего API из JavaScript
response = api.callEndpoint("endpoint_1");

Параметры переданные в аргументах функции callEndpoint обладают наивысшим приоритетом, т.е. при подготовке параметров они идут перед теми, которые заданы другим способом, например, в настройках конечной точки или с помощью api сеттеров setHeaders / setRequestOptions / и т.д., или системными атрибутами бота.

При этом, все параметры суммируются в порядке приоритетов, за исключением тела запроса $body. Если аргумент $body указан в функции, то JS код из настроек конечной точки не учитывается, и в API в качестве тела запроса будет передано то, что указано в аргументе $body функции callEndpoint. 

Таким образом, $body можно вычислить заранее (или хранить где-то в атрибутах или таблице) и передавать в аргументе функции или вычислять его в скрипте который вызывает api.callEndpoint, а не вычислять во вложенном JS самого эндпоинта. Если перекрываем $body — то JS из эндпоита вызываться не будет для вычисления тела запроса, т.к. тело перекрыто аргументом обладающим наивысшим приоритетом.

Пример №2 вызова внешнего API из JavaScript с перекрытием $body и других параметров
api.setHeaders({"Token": "my token"});
response = api.callEndpoint("endpoint_2", null, 'POST', 'TEXT', "MY BODY TEXT");

URL конечной точки заданный в настройках внешнего API (в интерфейсе) будет задействован, только если не указан $uri в аргументах функции (для второго по счету аргумента функции callEndpoint задан параметр null). Таким образом пустая строка в аргументе для URL перекрывает параметры указанные в настройках точки.

Структура конечной точки внешнего API

Описание полей внешнего эндпоинта представлена в таблице ниже.

Поле Описание
Флаг активности Если убрать флаг, то вызов данного Endpoint не будет выполняться и метод api.callEndpoint(...) вернет null.
Код

Дополнительный уникальный идентификатор. Используется для указания объекта, например, в JavaScript функциях. В этом случае Код, используется как первый аргумент ($externalApiEndpointCode) метода api.callEndpoint(...).

 

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

URL конечной точки (Endpoint URL) Допустимо использование макропеременных.
JavaScript для вычисления API запроса (Request Body)

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


Это значит, что тело API запроса вычисляется на основе JavaScript. В JavaScript должен присутствовать ключевой оператор return для возврата значения из JS. Выражение которое указывается после return является телом API запроса (Request Body).


Можно возвращать:

  • Строку (будет сконвертирована в json, если формат валидный);
  • Массив;
  • Объект (в том числе JSON-объект);
  • Любое другое выражение (если в конечной точке указан формат TEXT), для результата вычисления которого возможна автоматическая конвертация в строку (например число или булево).

Пример (если формат запроса JSON):


return {
  "key": "value",
  "lead_name": lead.getData('name'),
  "request_param_name": request.json.name
};

Другой пример (если формат запроса TEXT):


let a = false;
return a;

В JS можно выполнять любой код, например обращаться к пользователям, или устанавливать переменные в memory, планировать задания бота (триггеры, рассылки, скрипты), а также вызывать внешнее API c помощью api.callEndpoint или другие методов объекта api: post/get/postJson/getJson/request.

Метод(тип) HTTP запроса

POST / GET / PATCH / и т.д.

Формат HTTP запроса

JSON / TEXT

Заголовки HTTP запроса (HTTP Headers)

Заголовки, которые будут переданы в HTTP запросе. В данном поле указывается JSON объект или массив. Допустимо использование макропеременных.


Также, можно воспользоваться JS методом api.setHeaders(...) или передать заголовки HTTP запроса прямо в аргументах функции api.callEndpoint(...). Также доступна установка заголовков HTTP запроса с помощью атрибута бота sys_api_headers.

Пример 1 для HTTP Headers8.png

В JavaScript устанавливаем memory атрибут:

memory.setJsonAttr("my_headers", {"token": "my_token"});

Далее используем этот атрибут в поле Headers с помощью макроподстановки:

{{ &$$my_headers }}

Обратите внимание, в данном примере выполняется полное заполнение JSON с помощью макроподстановки.

Пример 2 для HTTP Headers

11.png


JavaScript:

memory.setAttr('someValue', 'my value');

Headers:

{
     "some_param": "{{ &$someValue }}",
     "other_param": 11
}

Обратите внимание, в данном примере выполняется частичная замена JSON с помощью макроподстановки.

Пример 3 для HTTP Headers

12.png


JavaScript:                        

memory.setAttr('anyJsonString', ', "param_3": "my new param"');

Headers:

{
    "some_param": "lead.getAttr('any_lead_param')",
    "other_param": 11
    {{ &$anyJsonString }}
}

Обратите внимание, в данном примере выполняется частичная замена JSON с помощью макроподстановки c атрибутом лида, а также подход  похожий на конкатенацию строк, где к JSON клеится строка записанная в атрибут anyJsonString. 

Параметры HTTP запроса (HTTP Request Options)

Параметры HTTP запроса. В данном поле указывается JSON объект или массив.

Допустимо использование макропеременных.

 

Также, можно воспользоваться v8 методом api.setRequestOptions(...) или передать параметры HTTP запроса в аргументах функции api.callEndpoint(...)

Также доступна установка параметров HTTP запроса помощью переменной бота sys_api_request_options.

Пример 1 для HTTP Request Options

13.png

JavaScript:

memory.setAttr('leadName', lead.getData('name'));
memory.setAttr('addQueryParams', ', "lead_id":' + lead.getData('id'));

HTTP Request Options:

{
  "query": {
    "lead_name": "{{ &$leadName }}",
    "new_param": 11
    {{ &$addQueryParams }}
  }
}

Пример 2 для HTTP Request Options

14.png

 JavaScript:

memory.setJsonAttr('headers', {
  "token": "aasdsad#$%^&*()"
});

memory.setJsonAttr('request_options', {
  "query": {
    "lead_id": lead.getData('id'),
    "lead_name": lead.getData('name'),
    "new_param": 11,
  }
});

HTTP Request Options:

{{ &$$request_options }}
Конфиг HTTP клиента (HTTP Client Config)

Параметры HTTP клиента.

В данном поле указывается JSON объект или массив.

Допустимо использование макропеременных.


Также, можно воспользоваться v8 методом api.setHttpConfig(...) или передать конфиг HTTP клиента в аргументах функции api.callEndpoint(...)

Также доступна установка конфига HTTP клиента с помощью переменной бота sys_api_http_client_config.


Пример 1 для HTTP Client Config


15.png

Настройки прокси (Proxy URL)

Используется для того, чтобы обращаться к определенному ресурсу с помощью прокси сервера.

 

Шаблон для указания прокси:

socks5://login:passwd@www.host.com:port

Также, можно воспользоваться v8 методом api.setProxyUrl(...) или передать Proxy URL для HTTP запроса в аргументах функции api.callEndpoint(...). Также доступна установка прокси с помощью переменной бота sys_api_proxy_url.

Комментарий

Любой текст. Доступно указание любых HTML-тэгов.

9.png

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

Проверка работы внешнего API

Для тестирования внешнего API можно воспользоваться любым онлайн сервисом для отладки вебхуков (или развернуть аналогичный локально):

Пример:

10.png

С API так же можно работать с помощью JS методов. Все методы и их описание вы можете посмотреть в Справочнике по функциям JS

Metabot API

Обработка ошибок API

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

Использование конструкций try..catch не поддерживается платформой на данный момент. Вы, конечно, можете так оборачивать код, но это ни к чему не приведет. Платформа сама, на системном уровне, обеспечивает безопасное выполнение кода и обработку исключительных ситуаций. Мы позаботились об этом: "под капотом" все безопасно, обернуто try..catch. Не нужно беспокоится, что код упадет по непонятным вам причинам. 

Ошибки в чат-боте будут только там, где они должны возникать (например, ошибка в коде, синтаксисе, в обработке данных). Для всех случаев платформа Metabot вернет вызывающей стороне 500 ошибку. 

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

Все возможные коды ответа сервера

Код ответа HTTP

Причина

Возможны ли доп. поле в тела ответа

200

Все хорошо. В теле запроса возможны дополнительные ответы.

Да

401

Отсутствуют данные для авторизации или не верный токен авторизации.

Нет

403

Нет прав для выполнения запроса. Например, у пользователя API, для которого выдан токен, нет прав доступа к чат-боту.

Нет

404

Неправильный URL запроса. Возникает, когда точки интеграции нет или URL совсем не верный. Если URL верный, то значит кастомный внутренний эндпоинт не найден по алиасу.

Нет

500

Внутренняя ошибка. Например, ошибка в JavaScript коде чат-бота.

Нет

Тело ответа для 200 ответа

В случае успеха, чат-бот возвращает код ответа 200 OK, а в теле запроса, содержится информация об успешном выполнении, например:

{ 
  "success": true
}
Если есть ошибка, то также возвращается 200 ОК и дополнительно возвращается текст ошибки в поле message, например:

{ 
  "success": false,
  "message": "Сообщение об ошибке"
}

В случае, если необходимо возвращать код ошибки, будет добавлен код ошибки, например:

{ 
  "success": false,
  "errorCode": 422,
  "message": "Ошибка валидации"
}


Плагины

Инструкция пользователя

Плагины Metabot — это JavaScript библиотеки, скрипты которых доступны для повторного использования в различных скриптах ботов.

Существует два вида плагинов:

Другими словами, вы можете создать свои собственные JavaScript библиотеки или использовать предопределенные, готовые библиотеки.

Список общих плагинов

Наименование Подключение Пример Дополнительно
moment  require() let moment = require('moment')

moment-with-locales

require() let moment = require('moment-with-locales')

Это OpenSource библиотека для работы с датами, https://momentjs.com/

Версия библиотеки moment.js: 2.29.3

Common.Bot.Commands

require() | snippet() require('Common.Bot.Commands') | snippet('Common.Bot.Commands')

Если для вашей ситуации доступно подключение через require – то это более предпочтительный с точки зрения производительности вариант, используйте именно его. Если подключение с помощью require() недоступно или не приемлемо, то используйте подключение через snippet().

На данный момент подключение собственных библиотек бизнеса доступно только через snippet().

Интерфейс и логика настройки плагинов

image.png

Интерфейс плагинов на платформе Metabot24 состоит из двух уровней:

image.png

image.png

Полное имя библиотеки «клеится» из трех составляющих:

При подключении библиотеки «составляющие» разделяются точкой, а при обращении к методам библиотеки из JS «составляющие клеятся» вместе, т.е. пишутся слитно, без точки.

Сниппеты

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

Доступны два варианта подключения сниппета:

snippet("your_snippet_name")
или
[[:your_snippet_name:]]
Оба варианта работают как макрос, т.е.указанный вариант обьявления сниппета в JavaScript коде будет заменен на код, который указан в скрипте плагина, к которому мы обращаемся.

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

Рассмотрим сначала простой пример указания значения переменной, вместо JavaScript. Вместо JavaScript кода сниппета можно указать любую комбинацию, например:

let text = snippet("mysnippet");
А в самом сниппете указать любое значение (число, текст, булево и т.п.), например, если в сниппете мы указали код:

"Hello World!"
То, запускаемый JS интерпретатором код примет следующий вид:

let text = "Hello World!"
Обратите внимание, что символ точка с запятой является частью определения макроса, и может указываться после snippet, а может не указываться. Если вам необходимо чтобы «конечный исходный код», после замены содержал точку с запятой, то укажите ее в самом JavaScript коде сниппета (скрипта плагина), т.е.:

"Hello World!";
Тогда, запускаемый JS интерпритатором код примет следующий вид:

let text = "Hello World!";

Сниппет идентичен макроподстановке. Макроподстановка выполняется до запуска скрипта и не является командой интерпретатора JavaScript, поэтому если вы закомментируете объявление сниппета, то он все равно будет подставлен в исходный код ! Чтобы закомментировать сниппет нужно закомментировать его и нарушить синтаксис его объявления, чтобы система не нашла макрос со сниппетом и не выполнила макроподстановку, например написать: s!nippet("your_snippet_name").

Пример №1: Общий плагин (скрипт общего плагина)

Полное имя библиотеки «клеится» из трех составляющих:

Подключается такая библиотеки с помощью кода:

require('Common.Bot.Commands');

После подключения, в JavaScript автоматически будет доступна переменная CommonBotCommands.

К методам CommonBotCommands можно обращаться, например для отправки сообщения в мессенджер из JavaScript:

CommonBotCommands.SendText('Текст отправленный из V8');

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

В данном примере речь идет именно об общей библиотеке (Common), внутри которой уже описан специальный объект, который можно использовать с помощью методов, чтобы добиться такого же варианта использования вы должны самостоятельно описать свой объект на JavaScript. Оформление в виде JavaScript обьекта при создании вашего собственного плагана бизнеса не является обязательным, за счет того что ваши скрипты будут подключаться в виде сниппетов. Вы можете просто описать любые переменные и функции в вашей библиотеке и обращаться к ним по имени.

Для библиотек уровня бизнеса автоматически создаваемые переменные-объекты не доступны (или доступны только в тех библиотеках которые для вас создаст команда Metabot).

Отметим, что существуют другие библиотеки, например, такие как moment.js. Подключение и использование таких библиотек отличается от использования библиотек созданных на платформе. Например, для moment.js, не нужно указывать три «состявляющие» названия скрипта (уровень доступа, плагин и скрипт). Для moment.js мы явно объявляем переменную и «экспортируем в нее библиотеку»:

let moment = require('moment');

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

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

Пример №2: Плагин бизнеса (скрипт вашего плагина)

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

image.png

Заполним все необходимые поля.

image.png

Далее, перейдем в скрипты плагина.

image.png

И создадим новый скрипт.

image.png

Заполним в открывшемся окне необходимые поля.

image.png

Укажем в исходном коде скрипта:

const greetMsg = 'Привет, ' + lead.getData('name') + '!';
memory.setAttr("greet", greetMsg);

Здесь мы сохраняем текст приветствия в memory (временном атрибуте бота).

Полное имя библиотеки «клеится» из трех составляющих:

Подключается с помощью кода:

snippet('Business.Notifications.HelloLead');
image.png
Для проверки работы плагина, в скрипт бота необходимо добавить две команды:
snippet('Business.Notifications.HelloLead');
{{ &$greet }}
image.png

Результат работы скрипта в телеграм:

image.png

Пример №3: Использование общего плагина в плагине бизнеса

Модифицируем пример 2 так, чтобы текст отправлялся не с помощью команды Отправить текст, а прям из JS кода плагина.

image.png

В исходном коде скрипта укажем:

require('Common.Bot.Commands');
const greetMsg = 'Привет, ' + lead.getData('name') + '!';
CommonBotCommands.sendText(greetMsg);

image.png

Удалим команду Отправить текст, команду Выполнить JavaScript оставляем без изменений.

image.png

Результат работы данного скрипта будет идентичным примеру 2:

image.png


Библиотека плагинов

Библиотека плагинов

Плагин для Mindbox

Название плагина Mindbox
Разработчик Официальные плагины от Metabot
Авторы

Петрова Ирина Дмитриевна (ira.petrova@metabot.org)

Гарашко Артем Юрьевич (artem@metabot.org)

Дата создания 22 Ноября 2022
Последняя дата обновления 26 Ноября 2022

Описание

Mindbox — это платформа автоматизации маркетинга и клиентских данных. Этот плагин к платформе Metabot позволяет интегрировать ваш чат-бот, разрабатываемый на Metabot, с платформой Mindbox. Подробнее с Mindbox можно ознакомиться на их официальном сайте: https://mindbox.ru.

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

Подключение

Для интеграции Mindbox с вашим чат-ботом вам необходимо сделать несколько вещей:

  1. На стороне Mindbox настройте все нужные точки интеграции и операции. Подробнее смотрите в документации Mindbox.
  2. Зарегистрируйтесь на платформе Metabot, подтвердите почту, авторизуйтесь и создайте чат-бот.
  3. Используйте готовый общий плагин или создайте плагин для своего бизнеса и скопируйте в него наш исходный код. Инструкции для обоих вариантов будут указаны ниже.
  4. Создайте в чат-боте системный атрибут mindbox.secretKey.<Системное имя точки интеграции> и сохраните в него секретный ключ (токен) авторизации API запросов к Mindbox. 
    • Если у вас планируется несколько точек интеграции в чат-боте, то нужно задать свой ключ для каждой из них. Например, так:
      • Mindbox.SecretKey.MyBusiness.Chatbot - атрибута с ключом для точки MyBusiness.Chatbot
      • Mindbox.SecretKey.MyBusiness.Chatbot.Shop - атрибута с ключом для точки MyBusiness.Chatbot.Shop
    • Внимание! Называйте атрибуты в точности и с учетом регистра именно так, как они указаны в Mindbox. 
  5. Реализуйте диалоговый сценарий (скрипт) с опросом данных пользователя (например, имя, фамилия, email адрес, телефон и прочее). 
  6. В конце диалога, обязательно (!) запросите согласие пользователя на обработку персональных данных и пришлите ссылку на положение о конфиденциальности компании. Если подписываете на маркетинговую рассылку, то дополнительно запросите согласие. Храните согласие в атрибуте лида вашего чат-бота на случай, если это понадобится юридической службе вашей компании. 

  7. Выберите универсальный или упрощенный метод для генерации запроса к Mindbox (смотрите детали ниже) и скопируйте код примера к себе в скрипт. 
  8. Кастомизируйте код интеграции под свои задачи. Если у вас возникнут затруднения, обратитесь за помощью в Телеграм-чат cообщества или поддержку Metabot.  

Вызов в диалоге

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

Способ 1. Универсальная (JSON) функция

Первый способ использования плагина в диалоге — с помощью универсальной функции callMindboxEndpoint(). Вы сами формируете тело запроса в формате JSON и передаете его в функцию.

Пример использования универсального (JSON) метода:

let email = lead.getAttr("email")
let phone = lead.getAttr("phone")
let lastName = lead.getAttr("lastName")
let firstName = lead.getAttr("firstName")
let endpointId = "<Идентификатор точки интеграции>"
let operation = "<Системное имя операции>"

let jsonBody = { "customer": {
    "email": email,
    "mobilePhone": phone,
    "lastName": lastName,
    "firstName": firstName,    
    "customFields": {
       "AdCommunicationAgreement": true,
       "PersonalDataAgreement": true
    },
    "subscriptions": [
      {
        "brand": "<Системное имя бренда подписки клиента>",
        "pointOfContact": "<Системное имя канала подписки: Email, SMS, Viber, Webpush, Mobilepush>",
        "topic": "<Внешний идентификатор тематики подписки>"
      }
    ]
  },
  "pointOfContact": "<Внешний идентификатор точки контакта>"
 }

// Подключаем сниппет кода из плагина Mindbox
snippet('Common.Mindbox.Operations')

// Вызываем точку интеграциии Mindbox и передаем нужный нам JSON
callMindboxEndpoint(endpointId, operation, jsonBody)

Функция требует передачи трех переменных в строгом порядке:

endpointId

Уникальный идентификатор интеграции.

Не забудьте, что каждому endpointId  соответствует свой secretKey, который нужно сохранить в системную атрибуту бота.

Интеграции настраивается в системе Mindbox.

operation Название операции в Mindbox. Каждому типу действия в Mindbox соответствует своя операция.
Список операций настраивается в системе Mindbox.
jsonBody Тело запроса в формате JSON.
Формат тела запроса, различается в зависимости от типа операции.

Способ 2. Альтернативная (параметризованная) функция

Второй способ использования плагина — с помощью альтернативной функции callMindboxEndpointAlt(), в которую вы передаете параметры запроса, а функция внутри себя формирует тело запроса в формате JSON и затем вызывает описанную выше универсальную функцию. 

Пример использования альтернативного (параметризованного) метода:

let email = lead.getAttr("email")
let phone = lead.getAttr("phone")
let lastName = lead.getAttr("lastName")
let firstName = lead.getAttr("firstName")
let endpointId = "<Идентификатор точки интеграции>"
let operation = "<Системное имя операции>"

// Подключаем сниппет кода из плагина Mindbox
snippet('Common.Mindbox.Operations')

// Вызываем точку интеграциии Mindbox и передаем нужные нам параметры
callMindboxEndpointAlt(endpointId, operation, 
                       email, phone, lastName, firstName, 
                       subscriptionTopic, pointOfContact)

Функция требует передачи нескольких переменных в строгом порядке. Добавляйте и удаляйте параметры по вкусу ;) но при этом не забудьте поменять в плагине код формирования JSON.

endpointId

Уникальный идентификатор интеграции.

Не забудьте, что каждому endpointId  соответствует свой secretKey, который нужно сохранить в системную атрибуту бота.

Интеграции настраивается в системе Mindbox.

operation Название операции в Mindbox. Каждому типу действия в Mindbox соответствует своя операция.
Список операций настраивается в системе Mindbox.
email Email пользователя
phone

Мобильный телефон

lastName

Фамилия

firstName

Имя

subscriptionTopic

Внешний идентификатор тематики подписки. Настраивается в Mindbox.

pointOfContact

Внешний идентификатор точки контакта. Настраивается в Mindbox.

Варианты подключения плагина

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

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

snippet('Common.Mindbox.Operations');

Либо, вы можете создать свой плагин в вашем бизнесе, скопировать наш и изменив код под себя, для этого:

  1. Cоздайте плагин в вашем бизнесе и назовите его Mindbox.
  2. Создайте в плагине скрипт и назовите его Operations.
  3. Скопируйте код, размещенный ниже, в код нового скрипта.

В этому случае, в примерах, указанных выше, замените вызов общего сниппета на ваш собственный:

snippet('Business.Mindbox.Operations');  

В обоих случаях, перед вызовом сниппета требуется объявить все необходимые переменные и передать им соответствующие значения.

При успешном запросе во вкладке "Клиенты" в Mindbox будет создан клиент с переданными из чат-бота данными о нем: 

image.png

Так же, во вкладке "Действия" вы сможете найти историю выполненной операции:

image.png

Обработка ошибок

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

Хранилище  Название атрибута Описание
lead plugin.mindbox.lastError.code

В случае ошибки, будет содержать код ошибки плагина. Смотрите таблицу ошибок ниже.
bot
lead plugin.mindbox.lastError.message

В случае ошибки, будет содержать сообщение об ошибке плагина.  Смотрите таблицу ошибок ниже.
bot
Список ошибок

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

Код ошибки Сообщение об ошибке Рекомендации
1

Ошибка вызова Mindbox API ({код ошибки}).

Плагин вернет код ошибки Mindbox API. Поскольку чат-бот не запоминает детали вызова API потому что вызовы асинхронны (async), при получении данной ошибки детали смотрите на стороне Mindbox. 


Все возможные коды ошибок Mindbox API cмотрите в документации к Mindbox: https://developers.mindbox.ru/docs/error_processing


Подробнее про вызов операции через Mindbox API, который применяется в этом плагине, смотрите по ссылке: https://developers.mindbox.ru/docs/v3

2

Не задан ключ интеграции {имя атрибута} в атрибутах бота.

Проверьте не ошиблись ли вы в регистре символов или названии.

3

Не заданы обязательные параметры параметризованного метода.

Проверьте не пытаетесь ли вы передать пустые данные в Mindbox в одном из полей (смотрите поля в атрибутах лида). А если параметр допустимо передавать пустым, удалите соответствующую валидацию в коде в callMindboxEndpointAlt(). 

Исходный код плагина

Cкопируйте указанный ниже код в скрипт Operations вашего плагина Mindbox и измените как вам требуется.

/**
 * Универсальная функция для регистрации операции в Mindbox через точку интеграции.
 * Используется явное указание тела запроса в формате JSON.
 * @param {string} endpointID - Идентификатор точки интеграции в Mindbox
 * @param {string} operation - Системное имя операции
 * @param {object} jsonBody - Тело запроса (берется из настроек операции в Mindbox)
 * @returns {bool} - Результат выполнения функции: успешно (true), проблемы (false).
 */
function callMindboxEndpoint(endpointId, operation, jsonBody) {
  // Считываем ключ авторизации из аттрибуты бота
  let secretKey = bot.getAttr('mindbox.secretKey.'+ endpointId)
  // Если ключ не настроен
  if (!secretKey) {
    outputError(endpointId, operation, '2', 'Не задан ключ интеграции plugin.Mindbox.secretKey в атрибутах чат-бота.')
    return false
  }
  
  // Задаем заголовок запроса
  api.setHeaders({'authorization':'Mindbox secretKey="' + secretKey + '"'})
 // Задаем URL запроса (используем асинхронный 'async' метод)
  let url = "https://api.mindbox.ru/v3/operations/async?endpointId=" + endpointId + "&operation=" + operation

  // Выполняем запрос с помощью метода POST и запоминаем результат
  let jsonResponse = api.postJson(url, jsonBody) 
  let jsonResponseCode = api.getLastResponseCode()

  // Возникла ошибка
  if (jsonResponseCode != 200) {
    outputError(endpointId, operation, '1', 'Проблема вызова Mindbox API. Детали смотрите в Mindbox.')
    return false // Выполнено с проблемами
  }
  
  return true // Все ок
}

/**
 * Альтернативная (параметризованная) функция для регистрации операции в Mindbox, который подготавливает тело запроса.
 * Адаптируйте эту функцию под свой проект или создайте копию.
 * @param {string} endpointID - Идентификатор точки интеграции в Mindbox
 * @param {string} operation - Системное имя операции
 * @param {string} email - Email
 * @param {string} phone - Мобильный телефон
 * @param {string} lastName - Фамилия
 * @param {string} firstName - Имя
 * @param {string} subscriptionTopic - Внешний идентификатор тематики подписки
 * @param {string} pointOfContact - Внешний идентификатор точки контакта
 * @returns {bool} - Результат выполнения функции: успешно (true), проблемы (false).
 */
function callMindboxEndpointAlt(endpointId, operation, email, phone, lastName, firstName, subscriptionTopic, pointOfContact) {
  // Если передали пустые значения
  if (isStrEmpty(endpointId) || 
      isStrEmpty(operation) || 
      isStrEmpty(email) || 
      isStrEmpty(phone) || 
      isStrEmpty(lastName) || 
      isStrEmpty(firstName) || 
      isStrEmpty(subscriptionTopic) || 
      isStrEmpty(pointOfContact))
  {
    outputError('3', 'Не заданы обязательные параметры параметризованного метода.')
    return false
  }
  
  // Формируем тело запроса в JSON (в вашем проекте запрос может отличаться)
  let jsonBody = { 
    "customer": {
      "email": email,
      "mobilePhone": phone,
      "lastName": lastName,
      "firstName": firstName,    
      "customFields": {
         "AdCommunicationAgreement": true, // Согласие на рассылку
         "PersonalDataAgreement": true     // Согласие на обработку персональных данных
      },
      "subscriptions": [
        {
          "brand": "<Системное имя бренда подписки клиента>",
          "pointOfContact": "<Системное имя канала подписки: Email, SMS, Viber, Webpush, Mobilepush>",
          "topic": subscriptionTopic
        }
      ]
    },
    "pointOfContact": pointOfContact
  }
  
  // Вызываем универсальную функцию и возвращем результат
  return callMindboxEndpoint(endpointId, operation, jsonBody)
}

/**
 * Вспомогательная функция, которая сохраняет сведения об ошибке в атрибутах лида и атрибутах бота.
 * @param {string} endpointId - Точка интеграции
 * @param {string} operation - Операция
 * @param {string} code - Код ошибки
 * @param {string} message - Сообщение об ошибке
 */
function outputError(endpointId, operation, code, message) {
  // Добавляем в конце сообщения доп. инфу.
  message = message + ' (точка=' + endpointId + ', операция=' + operation + ')'
  lead.setAttr("plugin.Mindbox.lastError.Code", code)
  lead.setAttr("plugin.Mindbox.lastError.Message", message)  
  bot.setAttr("plugin.Mindbox.lastError.Code", code)
  bot.setAttr("plugin.Mindbox.lastError.Message", message)   
}

/**
 * Вспомогательная функцию, которая проверяет является ли строка пустой.
 * @param {string} code - Код ошибки
 * @returns {bool} - если строка пустая (true), иначе (false).
 */
function isStrEmpty(str) {
  return (typeof str === 'string' && str.trim().length === 0) ? true : false
}


Библиотека плагинов

Диалоговое путешествие (Dialog Journey)

Название плагина Диалоговое путешествие (Dialog Journey)
Разработчик Официальные плагины от Metabot
Авторы

Гарашко Артем Юрьевич (artem@metabot.org)

Дата создания 04 Января 2023
Последняя дата обновления 06 Января 2023

Описание

Платформа Metabot — это платформа автоматизации коммуникаций. Плагин Dialog Journey (DJ) для платформы Metabot позволяет интегрировать в ваш чат-бот, разработанный на Metabot или на любой другой бот-платформе, поддерживающей вызовы и приемы API веб-хуков, возможность проектирования, отслеживания и визуализации клиентских путешествий, также называемых клиентскими путями (customer journeys).

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

Плагин позволит организовать развитие вашего чат-бота таким образом, чтобы все разрозненные диалоги объединялись в единую коммуникационную стратегию компании, представленную в виде путешествий (journeys), разбитых на фазы (phases) с целями (goals), предоставляемой пользой (values) и измеряемыми показателями (metrics). Более подробное описание концепции и устройства плагина, а также предлагаемой методологии маркетинговой стратегии смотрите ниже.

Пример

Пример работы чат-бота с интегрированным плагином можете посмотреть перейдя по ссылке.

Настройка 

Для интеграции плагина DJ с вашим чат-ботом вам необходимо сделать следующее:

  1. Создать кастомные таблицы и заполнить их, согласно схеме базы данных, опубликованной в разделе Справочники.
  2. Спроектировать путешествия для ваших клиентских сегментов и настроить справочники, описания назначения которых смотрите ниже.
    1. Классы персон.
    2. Подклассы персоны.
    3. Путешествия.
    4. Фазы путешествия.
    5. Польза.
    6. Цели.
    7. Показатели.
    8. Активности.
  3. Скачать готовый шаблон чат-бота, в который уже интегрирован плагин DJ, адаптировать его код и структуру под ваши задачи в своем чат-боте.
    1. Ссылка.
  4. Ознакомиться с примерами когда вызова методов DJ из вашего чат-бота в разделе JS команды.
  5. Ознакомиться с инструкций как пользоваться аналитикой и отчетами в разделе Аналитика и отчеты.   

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

  1. Чтобы ваш чат-бот мог передавать в базу данных Мetabot все нужные вам события для аналитики, например, сообщать, что пользователь достиг цели или перешел к следующей фазе путешествия.
  2. Чтобы ваш чат-бот мог запрашивать в базе данных Metabot информацию о состоянии путешествия пользователя, например, проверять достиг ли пользователь цели, были ли предоставлена польза, узнавать фазу путешествия на которой находится пользователь и так далее.

Инструкцию о том, как подключить Dialog Journeys к чат-боту (а может и не только к чат-боту), создаваемому на сторонней платформе, смотрите в разделе Интеграция со сторонними системами.

Методология 

Предисловие

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

Возможно модель подойдет к вашей бизнес-практике идеально «как есть». Мы постарались сделать модель простой и в то же время достаточно гибкой — принципы, лежащие в ее основе, универсальны и действенны.

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

Методология маркетинга, на которой строится работа данного плагина, стоит на нескольких китах. Во-первых, это решение для современного маркетинга для «экономики связей» или «экономики подключения» (Connection Economy). Подробнее об этой концепции смотрите здесь

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

Справочники

Классы персон

Проектирование начинается с описания клиентских сегментов — мы их называем Классы персон. Подобно, RPG играм, опишите все классы, с которыми имеет дело ваш бизнес.

image.png

Подклассы персон

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

image.png

Путешествия

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

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

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

Примеры путешествий:

image.png

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

Цели 

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

Пример целей для онбординга партнеров:

Пример целей для обучения стрельбе из луков:

image.png

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

Фазы путешествия

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

Пример фаз для онбординга нового партнера:

Пример фаз для учебы стрельбе из лука:

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

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

image.png

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

Польза 

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

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

image.png

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

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

Показатели

Спланируйте Показатели (или метрики), которые вам необходимо измерять.

Пример из жизни: 

Сказочный пример:

image.png

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

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

Активности

Активности — это события, которые происходят в ходе путешествия или действия, которые совершает пользователя, во время путешествия.

image.png

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

ID Код Название Пояснение
1 journeyStarted Путешествие начато Используется в самом начале путешествия, когда стало понятно, что пользователь "отправился в путешествие".
2 phaseStarted     Фаза начата Используется при запуске следующей фазы, а также автоматически в начале путешествия.
3 phaseCompleted     Фаза завершена  Используется при запуске следующей фазы, завершая предыдущую, и при завершении всего путешествия.
4 phaseInterrupted     Фаза прервана Используется при прерывании фазы, которое происходит либо при прерывании всего путешествия, либо когда по каким-то причинам в нам нужно будет прервать фазу.
5 phaseSkipped     Фаза пропущена  Используется по каким-то причинам когда вам необходимо пропустить целую фазу.
6 journeyCancelled     Путешествие отменено   Используется когда путешествие было отменено, например, пользователь передумал.
7 journeyCompleted     Путешествие завершено Путешествие успешно завершено, пользователь дошел до победного конца.

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

Журналы

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

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

Журнал путешествий 

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

image.png

Журнал пользы

В этом журнале хранится история предоставления пользы пользователям.

image.png

Журнал целей

В этом журнале хранится история достижения целей.

image.png

Журнал показателей 

В этом журнале хранятся собранные метрики.

image.png

Состояние пользователя в путешествии

В этой таблице хранится состояние пользователя в конкретном путешествии. 

image.png

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

JS команды

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

Список команд и примеры кода представлены в таблице ниже.

Название метода Пример кода
1

Начать путешествие

// Подключаем плагин
snippet("Common.DialogJourney.Manager")

// Создаем менеджер путешествия и передаем ему лид
let jm = new JourneyManager(lead)

// Начинаем новое
let isNewJourneyStarted = jm.startNewJourney("myJourney")

// Пишем результат в память
memory.setAttr("isNewJourneyStarted", isNewJourneyStarted)

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

В качестве роли персоны будет использована роль по умолчанию, которую необходимо задать в настройках чат-бота.

2

Завершить путешествие

// Подключаем плагин
snippet("Common.DialogJourney.Manager")

// Создаем менеджер путешествия и передаем ему лид
let jm = new JourneyManager(lead)

// Выбираем путешествие
jm.selectJourney("myJourney")

// Завершаем выбранно путешествие
let isJourneyCompleted = jm.completeJourney()

// Пишем результат в память
memory.setAttr("isJourneyCompleted", isJourneyCompleted)
3 Следующая фаза
// Подключаем плагин
snippet("Common.DialogJourney.Manager")

// Создаем менеджер путешествия и передаем ему лид
let jm = new JourneyManager(lead)

// Выбираем путешествие
jm.selectJourney("myJourney")

// Запоминаем название текущей фазы
let oldPhaseName = jm.getCurrentPhase().name

// Стартуем новыую фазу путешествия
let isPhaseStarted = jm.startNextPhase("nextPhaseCode")

// Запоминаем название новой фазы (если она установилась, конечно)
let newPhaseName = jm.getCurrentPhase().name

// Пишем результат в память
memory.setAttr("isPhaseStarted", isPhaseStarted)
memory.setAttr("oldPhaseName", oldPhaseName)
memory.setAttr("newPhaseName", newPhaseName)
4 Предоставить пользу
// Подключаем плагин
snippet("Common.DialogJourney.Manager")

// Создаем менеджер путешествия и передаем ему лид
let jm = new JourneyManager(lead)

// Выбираем путешествие
jm.selectJourney("myJourney")

// Сообщаем менеджеру путешествия, что мы предоставили пользу
let isValueGiven = jm.giveValue("valueCode")

// Загружаем информацию о метрике из справочника
let valueInfo = jm.getValueInfo("valueCode")

// Загружаем информацию о собранной метрике
let value = jm.getValue("valueCode")

// Выводим результат работы
memory.setAttr("isValueGiven", isValueGiven)
memory.setAttr("valueName", valueInfo.name)
memory.setAttr("valueWeight", value.weight)
5 Достигнуть цель
// Подключаем плагин
snippet("Common.DialogJourney.Manager")

// Создаем менеджер путешествия и передаем ему лид
let jm = new JourneyManager(lead)

// Выбираем путешествие
jm.selectJourney("myJourney")

// Сообщаем менеджеру путешествия, что мы достигли цели
let isGoalAchieved = jm.achieveGoal("goalCode")

// Загружаем информацию о цели
let goal = jm.getGoalInfo(goalCode)

// Выводим результат работы
memory.setAttr("isGoalAchieved", isGoalAchieved)
memory.setAttr("goalName", goal.name)
6 Записать показатель
// Подключаем плагин
snippet("Common.DialogJourney.Manager")

// Создаем менеджер путешествия и передаем ему лид
let jm = new JourneyManager(lead)

// Выбираем путешествие
jm.selectJourney("myJourney")

// Считываем значение метрики
let metricValue = lead.getAttr("metricValue")

// Сохраняем метрику
let isMetricSaved = jm.saveJourneyMetric("metricCode", metricValue)

// Загружаем информацию о метрике из справочника
let metricInfo = jm.getMetricInfo("metricCode")

// Загружаем информацию о собранной метрике
let metric = jm.getLatestJourneyMetric("metricCode")

// Выводим результат работы
memory.setAttr("isMetricSaved", isMetricSaved)
memory.setAttr("metricName", metricInfo.name)
memory.setAttr("metricValue", metric.value)

Другие примеры кода с использованием плагина смотрите в шаблоне чат-бота, доступного для скачивания здесь.

Интеграция со сторонними системами

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

Также, вы можете отправлять в Metabot информацию о событиях из других систем, чтобы фиксировать прохождение пользователя по пути, когда пользователь совершает нужное целевое действие на сайте (например, размещает товар в корзину, но не оплачивает). 

Для интеграции сторонних систем с вашим Dialog Journey в чат-боте на Metabot, необходимо настроить точки интеграции для всех необходимых событий, которые вам нужно регистрировать в Metabot. Информацию о точках интеграции смотрите в разделе Точки интеграции и конструктор API. В коде точки интеграции разработайте код согласно примерам выше.

Библиотека плагинов

Business.Helpers.Response

Плагин Business.Helpers.Response служит утилитой для стандартизации ответов API в бизнес-приложениях. Его основная цель - упростить создание последовательных и структурированных ответов для различных сценариев, возникающих во время взаимодействия с API.

/** 
 * Prepares and returns an API response object for a failed operation.
 * This should be used when an operation does not complete successfully,
 * with the provided error message included in the response.
 * 
 * @param {string} errorMessage - The error message to be included in the response.
 * @returns {Object} An object representing a failed operation response.
 */
function getErrorResponse(errorMessage) {
  return {
    success: false,
    message: errorMessage
  };  
}

/** 
 * Prepares and returns an API response object for a successful operation.
 * This should be used when an operation completes successfully 
 * and no additional data needs to be returned.
 * 
 * @returns {Object} An object representing a successful operation response.
 */
function getSuccessResponse() {
  return {
    success: true  
  };
}

/** 
 * Prepares and returns an API response object for a successful operation 
 * with additional JSON data. This should be used when an operation completes 
 * successfully and there is additional data to return in the response.
 * 
 * @param {Object} json - The JSON data to be included in the response.
 * @returns {Object} An object representing a successful operation response with additional data.
 */
function getSuccessResponseWithJson(json) {
  return {
      ...json,
      success: true
  }
}
Библиотека плагинов

Интеграция с Google Sheets

Первым делом необходимо создать новую таблицу в Google Sheets и добавить нового редактора api@metabot.org.

image.png

Далее копируем ID таблицы из адресной строки.

image.png

Копируем название листа.

image.png

Копируем название столбцов в таблице.

image.png

Метод для добавления нового столбца

Записываем в нужное место скрипта следующий код:

var GoogleSheetsService = require('Common.Integrations.GoogleSheets') // Плагин для работы с Google Sheets

GoogleSheetsService.sheetId = '11muAnepqhpRQ9ElE9CzC3E-edmf9JbRE3gwmBTDa5pE' // ID скопированный из таблицы
GoogleSheetsService.listName = 'list' // Название листа

// Параметры где ключ - название столбца, значение - данные которые занесутся в строку
let params = {
	"region": "Москвская область",
	"name": 'Тест',
	"age": "24",
	"city": "Москва",
}

let result = GoogleSheetsService.addRow(params) // Функция для добавления строк в таблицу

debug(result) // Вернётся результат выполенния с Id в строки в которую записались данные

Пример ответа:

{
    "status": "success",
    "message": "Row added successfully", // Сообщение, если есть ошибка - вернётся описание ошибки
    "rowId": 8 // Id в строки в которую записались данные
}

Метод для поиска и замены значения в ячейке

Записываем в нужное место скрипта следующий код:

var GoogleSheetsService = require('Common.Integrations.GoogleSheets') // Плагин для работы с Google Sheets

GoogleSheetsService.sheetId = '11muAnepqhpRQ9ElE9CzC3E-edmf9JbRE3gwmBTDa5pE' // ID скопированный из таблицы
GoogleSheetsService.listName = 'list' // Название листа

// Параметры со настройками для замены
let params = {
    colomn_search_name: 'region',
    colomn_edit_name: 'region',
    search_value: '123123',
    match_entire_cell: true,
    new_value: "Антон"
}

let result = GoogleSheetsService.searchAndEditRow(params) // Функция для поиска и замены строк

debug(result) // Вернётся результат выполенния или код ошибки

Пример ответа:

{
    "status": "success",
    "message": 'Значение найдёно и измененно'
}
Библиотека плагинов

Документация Telegram-плагина

Название плагина Telegram
Разработчик Официальные плагины от Metabot
Авторы

Борисов Павел (https://t.me/mr_result)

Дата создания 02 Октября 2023
Последняя дата обновления 25 Апреля 2024

Описание

Этот плагин предоставляет удобный интерфейс для работы с Telegram Bot API. Он поддерживает отправку текстовых сообщений, фотографий, создание клавиатур и обработку ответов пользователя.

Основной класс TelegramMessage

Подключение и инициализация

let TelegramMessage = require('Common.Integrations.Telegram')
let msg = new TelegramMessage()

Основные параметры

msg.text = "Ваше сообщение" // Текст сообщения
msg.parse_mode = "HTML" // Режим форматирования (HTML или MarkdownV2)
msg.protect_content = true // Защита контента от пересылки
msg.keyboard = "Да[yes]==Нет[no]" // Создание клавиатуры

Форматы клавиатуры

msg.keyboard = "Кнопка1[btn1]==Кнопка2[btn2]" // Кнопки будут расположены вертикально
msg.keyboard = "Кнопка1[btn1]||Кнопка2[btn2]" // Кнопки будут расположены горизонтально
msg.keyboard = "Кнопка1[btn1]||Кнопка2[btn2]==Кнопка3[btn3]||Кнопка4[btn4]"

Специальные типы кнопок

msg.keyboard = "Отправить контакт[telegram_contact]"
msg.keyboard = "Отправить локацию[telegram_location]"
msg.keyboard = "Посетить сайт[<https://example.com>]"
msg.keyboard = "Открыть приложение{<https://webapp-url.com>}"
msg.keyboard = "Написать админу[tg://user?id=123456789]"

Примеры использования

Простое меню с обработкой ответов

Код будет работать корректно только в команде JS Callback.

let TelegramMessage = require('Common.Integrations.Telegram')
let msg = new TelegramMessage()
msg.text = "Выберите действие:"
msg.keyboard = "Информация[info]==Помощь[help]==Настройки[settings]"
msg.parse_mode = "HTML"

switch (true) {
    case (isFirstImmediateCall):
        msg.send()
        return false

    case (msg.getMessagePayload()?.callback_data == "info"):
        msg.addReplyToText()
        bot.sendMessage("Информация о боте...")
        return false

    case (msg.getMessagePayload()?.callback_data == "help"):
        msg.addReplyToText()
        bot.sendMessage("Справка по использованию...")
        return false

    case (msg.getMessagePayload()?.callback_data == "settings"):
        msg.addReplyToText()
        bot.runScriptForLead(123, leadId) // Запуск скрипта настроек
        return false
}

Справочник методов

Основные методы отправки

Получение информации

Обработка ответов пользователя

// Проверка типа ввода
if (msg.getMessagePayload()?.input_type == "write") {
    // Пользователь написал текст
}

// Проверка нажатия кнопки
if (msg.getMessagePayload()?.callback_data == "button_id") {
    // Пользователь нажал кнопку
}

Отладка

msg.debug("Отладочное сообщение") // Отправка отладочной информации. 
								  //Не отправляется пользователю но логируется в карточке лида

Лучшие практики

if (isFirstImmediateCall) {
    msg.send()
    return false
}
if (msg.getMessagePayload()?.input_type == "write") {
    bot.sendMessage('Пожалуйста, используйте кнопки')
    return false
}
msg.addReplyToText() // После нажатия на кнопку записывает её в сообщение

Форматирование текста и entities

В плагине доступно два способа форматирования текста: HTML и MarkdownV2. По умолчанию параметр parse_mode не установлен — это сделано специально для возможности работы с entities (когда нужно сохранить форматирование текста, которое пользователь отправил в Telegram).

HTML форматирование

Документация: https://core.telegram.org/bots/api#html-style

msg.text = "<b>Жирный текст</b>\n<i>Курсив</i>\n<code>Моноширинный текст</code>"
msg.parse_mode = "HTML"

MarkdownV2 форматирование

Документация: https://core.telegram.org/bots/api#markdownv2-style

msg.text = "**Жирный текст**\n__Курсив__\n`Моноширинный текст`"
msg.parse_mode = "MarkdownV2"

Работа с entities

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

Пример сохранения форматированного сообщения пользователя:

let TelegramMessage = require('Common.Integrations.Telegram')
let msg = new TelegramMessage()

msg.text = `Введите текст для рассылки`
msg.keyboard = `↩️ Назад[${backScripts}]`
msg.parse_mode = 'HTML'

switch (true) {
    case (isFirstImmediateCall):
        msg.send()
        return false

    case (msg.getMessagePayload()?.input_type == "write"):
        msg.removeInlineKeyboard()
        // Получаем webhook и извлекаем текст с entities
        let webhook = msg.getMessagePayload()
        let messageData = {
            "text": webhook?.payload?.message?.text,
            "entities": webhook?.payload?.message?.entities // Сохраняем entities для сохранения форматирования
        }

        // Сохраняем сообщение с форматированием в базу данных
        lead.setJsonAttr("messageData", messageData)

        bot.run({
            script_id: nextScript
        })
        return false

    default:
        msg.addReplyToText()
        bot.run({
            script_id: msg.getMessagePayload().callback_data
        })
        return true
}

В этом примере:

  1. Когда пользователь отправляет форматированное сообщение, мы получаем не только сам текст, но и массив entities.
  2. Entities содержат информацию о форматировании: жирный текст, курсив, ссылки и т.д.
  3. Мы сохраняем и текст, и entities, чтобы позже можно было воспроизвести сообщение с точно таким же форматированием.
  4. Важно: для работы с entities параметр parse_mode должен быть не установлен (режим по умолчанию).

Теперь после запоминания entities мы можем сделать его вывод таким образом:

let TelegramMessage = require('Common.Integrations.Telegram')
let msg = new TelegramMessage()

let messageData = lead.getJsonAttr("messageData")

msg.text = messageData?.text
msg.entities = messageData?.entities

// Если попробовать использовать parse_mode то форматирование будет некорректным

Работа с файлами

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

Получение и обработка файлов

// Подключаем библиотеку
let TelegramMessage = require('Common.Integrations.Telegram')
let msg = new TelegramMessage()

// Все входящие файлы
let attachments = bot.getAllAttachments()

if(Boolean(attachments?.[0]?.url)){

uploadData = bot.downloadFileFromUrl(attachments[0].url)

msg.debug('Пользователь отправил файл: ' + uploadData.url)
msg.debug('Файл ID : ' + JSON.stringify(attachments[0]?.payload?.file_id)) // данная строка вспомогательная для
																		   // автоматизации вывода полученных 
lead.setAttr('file', uploadData.url)									   // файлов в других местах бота
// Запуск скрипта...
bot.runScriptByCodeForLead("recieveFile", lead.getData('id'))

bot.stop()
}

В этом примере:

  1. Проверяем наличие прикрепленных файлов с помощью bot.getAllAttachments().
  2. Если файл есть, скачиваем его через bot.downloadFileFromUrl().
  3. Логируем получение файла с помощью msg.debug().
  4. Сохраняем URL файла в атрибут лида.
  5. Запускаем скрипт обработки файла.
  6. Останавливаем текущий скрипт.

Дополнительный пример:

let TelegramMessage = require('Common.Integrations.Telegram')

let msg = new TelegramMessage()

msg.text = 'А теперь жду твоё фото 😉'
msg.keyboard = ``
msg.parse_mode = 'HTML'

let scriptCode = bot.getCurrentScriptCode()
lead.setAttr("script_code", scriptCode)

let data = bot.getAllAttachments()

switch (true) {

    case (isFirstImmediateCall):

        msg.send()
        return false
        break

    case (Boolean(data?.[0]?.type != 'image')):

        bot.sendMessage('Отправь сжатое изображение. Такой формат не подходит')
        return false
        break

    case (Boolean(data?.[0]?.type == 'image')):

        uploadData = bot.downloadFileFromUrl(data[0].url)
        msg.debug('Пользователь отправил файл: ' + uploadData.url)
        lead.setAttr('photo', uploadData.url)
        bot.sendMessage('Класс! Фото загрузил')
        return true
        break

    default:

        msg.addReplyToText()
        return true
}

Рекомендации по работе с файлами

Web-формы в чат-боте и Web Apps

Web-формы в чат-боте и Web Apps

Введение. Виды форм. Принцип работы.

Теоретическая часть для разработчика сайта (разработчика HTML-формы) и для разработчика бота.

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

Актуальный исходный код веб-формы реализующий все три вида форм смотрите по ссылке: go-to-the-mars.html

Пример работы веб-формы приведенной выше смотрите в Telegram боте https://t.me/metabot_test_form_bot

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

  1. Перед началом разработки необходимо выбрать один из вариантов реализации формы и согласовать с разработчиком веб-формы и разработчиком чат-бота (если форма и чат-бот разрабатываются параллельно). Также необходимо согласовать формат обмена данными. 
  2. После того как вы определились с видом формы, необходимо разработать HTML-форму, разместить его на вашем сервере, подключить ее к backend вашего сайта (для обработки AJAX / API запросов), чтобы backend мог принять данные с формы и отправить их в API в Metabot. 

Для разработчика формы: реализация любого вида формы не имеет принципиальных архитектурных отличий, одна и также веб-форма может использоваться для любого вида формы путем добавления не сложных разветвлений в JS-коде веб-формы. Отличие только в том, что вариант с keyboad-кнопкой не требует дополнительного слоя для backend, но при этом имеет ряд ограничений, подробнее см в описании этого вида формы ниже. Мы рекомендуем использовать вариант Web App с inline-кнопокй + универсальную форму для любого мессенджера.

Для разработчика бота: см отдельную страницу документации для реализации необходимого вида формы в боте.

Дополнительное изучение возможностей работы Telegram Web Apps.

Универсальная форма в виде ссылки

Ключевая особенность: универсальный вариант, подходит для любого мессенджера.

Форма открывается в отдельной странице браузера. Как обычная страница web-браузера.

Для разработчика бота: вы можете добавить в боте условия, проверяя текущий мессенджер лида, чтобы открывать для нативного Telegram-канала форму с помощью inline кнопки, а для других каналов отправлять ссылку на форму в виде сообщения. Учтите, что в других мессенджерах нельзя удалять сообщения, поэтому заранее предусмотрите время жизни хэш-кода лида который указывается в ссылке на форму

Для Telegram ссылку можно отправить в виде inline-кнопки, но это все равно будет не "Web App приложение", а обычная страница открытая в браузере.

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

Если вы планируете использовать формы только в Telegram, то можете пропустить данный вид формы и перейти к описанию формы на основе Web App, открывающейся с помощью inline-кнопки. Но, желательно понимать отличия и изучить весь раздел документации.

Принцип работы, по шагам:

  1. Бот генерирует ссылку с уникальным хэш-кодом, привязанным к лиду.

  2. Бот отправляет эту ссылку в мессенджер в виде обычного сообщения.

  3. Пользователь бота нажимает на ссылку, открывается браузер с формой.

  4. Пользователь бота заполняет форму и нажимает кнопку для отправки формы.

  5. Форма отправляет данные на backend сайта, где размещена форма.
  6. Backend сайта отправляет данные формы в API Metabot, в данные должен быть включен уникальный хэш лида.
  7. Metabot принимает и сохраняет данные заполненной формы .
  8. Если все ок, то страница с формой в браузере должна быть закрыта, это делается с помощью простого JS кода (подробности рассматриваются на отдельной странице документации с описанием создания HTML-формы).

Пример JS кода для открытия ссылки на Telegram бота и закрытия страницы

  <script>
    function closeForm() {
        location.href="tg://resolve?domain=metabot_test_form_bot";
        window.close();
    }
  </script>

После заполнения формы, данные введенные пользователем необходимо отправить на бэк вашего сайта, а затем в Metabot API используя токен бота (чтобы токен бота не фигурировал в коде HTML-формы). Далее страницу браузера необходимо автоматически закрыть, с помощью JS-кода встраиваемого в HTML-форму. Рекомендуется отправлять данные на бэк с помощью REST API, для того чтобы после отправки не выполнять дополнительный рендеринг страницы (чтобы потом ее просто закрыть). Т.е. отправляем данные на бэк, убеждаемся что ответ от API - 200, т.е. все ОК и сразу закрываем форму, пользователь возвращается в бота. При этом желательно учесть в коде HTML-формы варианты, если что-то пошло не так, или пользователь остался на форме, а хэш код лида для формы истек (если вы генерируете токен со сроком действия), в этих случаях можно вывести сообщение о том что форма устарела, что пользователю необходимо вернуться в бота и запросить форму повторно, также дополнительно можно уведомить Metabot по API, чтобы бот автоматически выслал ссылку на новую форму.

Есть нюанс: перед закрытием формы можно вызвать открытие ссылки для перехода в мессенджер (на случай если после закрытия страницы не будет открывается мессенджер). Но, для PC, например, открытие ссылки с Telegram ботом не срабатывает автоматически и у пользователя остается открытой другая вкладка в браузере (если у него включено открытие ссылок мессенджера во внешнем браузере и в браузере открыто несколько вкладок).

Cсылка может быть отправлена в бота в виде простого текста или inline-кнопки Telegram, cодержащей ссылку на форму. Но в любом случае, для рассматриваемого варианта формы, нажатие на ссылку или кнопку приведет к открытию странице в браузере и после заполнения формы страницу необходимо автоматически закрывать. Здесь речь идет именно об обычной inline-кнопке c ссылкой для Telegram, а не о Telegram WebApp-форме, на основе inline-кнопки которая открывает форму в Web App (во всплывающем окне, на котором расположен WebView). Описание вариантов форм с WebApp смотрите ниже.

Информация для разработчика бота:

Отличие между Inline Web App Button и обычной inline кнопки в виде ссылки заключается в том, что Inline Web App Button формируется передачей параметров web_app и url а для обычная inline-кнопка в виде ссылки просто передачей параметра url. Детали можно найти в описании API Telegram.

https://core.telegram.org/bots/webapps#inline-button-web-apps
https://core.telegram.org/bots/api#inlinekeyboardbutton
https://core.telegram.org/bots/api#inlinekeyboardmarkup
https://core.telegram.org/bots/api#sendmessage

Inline кнопка с ссылкой для Telegram реализуется на основе JS Callback команды бота и метода bot.sendMessage или с помощью плагина Common.TelegramComponents.MenuHelper.sendMessage с передачей дополнительных параметров для работы inline-кнопки как ссылки.

Отличие bot.sendMessage от плагина Common.TelegramComponents.MenuHelper.sendMessage в том что функция плагина сама внутри вызывает bot.sendMessage, и при этом автоматически сохраняет ID последних сообщений в атрибутах лида. Этот атрибут нужен для использования в коде JS Callback, а также маршруте бота, если требуется удаление кнопки-ссылки на форму или самого сообщения с ссылкой. Функционал хранения идентичен работе с Фото-слайдером для Telegram (описано в документации по работе с командой JS Callback и файлами Telegram).

Команда JS callback необходима для обработки нажатия других кнопок которые могут быть отправлены вместе с кнопкой-ссылкой на форму или для fallback, если форму не заполнили, но отправили любой текст в мессенджер. 

Команда бота JS Callback не должна в данном случае обрабатывать событие отправки формы. Т.к. событие отлавливается с помощью Internal API Endpoint. В JS Callback можно отлавливать событие заполнения формы только для формы реализованной на основе keyboad-кнопки в Telegram.

WebApp-форма, на основе inline-кнопки

Данный вид формы работает только в Telegram.

Рекомендуемый вид формы для Telegram бота.

Ключевая особенность: форма открывается в всплывающем окне на котором расположен WebView (встроенный браузер).

Отличие данного варианта от универсальной формы в виде ссылки, в том, что форма работает как Web App, т.е. это специальный режиме браузера встроенного в Telegram. Этот режим гарантирует возврат в бота после заполнения формы, а также не позволит открыть ссылку во внешнем браузере (а также включает дополнительный функционал для взаимодействия веб-страницы с ботом). В обычном же режиме для универсальной формы, нет гарантий, что когда веб-страница будет закрыта, пользователя перекинет в бота.

Принцип работы, по шагам:

  1. Бот генерирует ссылку с уникальным хэш-кодом, привязанным к лиду.

  2. Бот отправляет эту ссылку в виде inline-кнопки в Telegram-мессенджер.

  3. Пользователь бота нажимает на кнопку, открывается Web App с формой (т.е. пользователь остается в Telegram, в мессенджере открывается встроенный браузер).

  4. Пользователь бота заполняет форму и нажимает кнопку для отправки формы.

  5. Форма отправляет данные на backend сайта, где размещена форма.
  6. Backend сайта отправляет данные формы в API Metabot, в данные должен быть включен уникальный хэш лида.
  7. Metabot принимает и сохраняет данные заполненной формы.
  8. Если все ок, то страница с формой в браузере должна быть закрыта, это делается с помощью простого JS кода (подробности рассматриваются на отдельной странице документации с описанием создания HTML-формы).

WebApp-форма, на основе keyboard-кнопки

Данный вид формы работает только в Telegram.

Альтернативный вид формы для бота в Telegram. Используется, если для вас крайне затратно реализовать дополнительный backend-слой для отправки данных в Metabot API

Имеет ряд ограничений и неудобств (список смотрите ниже)

Ключевая особенность: форма открывается в всплывающем окне на котором расположен WebView (встроенный браузер). Для отправки данных в Metabot API не требуется дополнительный backend.

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

Отличие данного варианта от универсальной формы в виде ссылки, в том, что форма работает как Web App, т.е. это специальный режиме браузера встроенного в Telegram. Этот режим гарантирует возврат в бота после заполнения формы, а также не позволит открыть ссылку во внешнем браузере (а также включает дополнительный функционал для взаимодействия веб-страницы с ботом). В обычном же режиме для универсальной формы, нет гарантий, что когда веб-страница будет закрыта, пользователя перекинет в бота.

Форма открываемая с помощью keyboard-кнопки (так называемой "кнопки-калькулятора"), это упрощенный вариант, чтобы не подключать дополнительный back-end к HTML-форме для пересылки результата сбора данных с HTML-формы в Metabot API. На первый взгляд такой вариант более простой, но имеет ограничения, описанные ниже, поэтому рекомендуется для Telegram использовать вариант WebApp-формы открываемой с помощью inline-кнопки.

Принцип работы, по шагам:

  1. Бот генерирует ссылку с уникальным хэш-кодом, привязанным к лиду.

  2. Бот отправляет эту ссылку в виде keyboard-кнопки в Telegram-мессенджер ("кнопка-калькулятор" отображаемая в нижней части мессенджера).

  3. Пользователь бота нажимает на кнопку, открывается Web App с формой (т.е. пользователь остается в Telegram, в мессенджере открывается встроенный браузер).

  4. Пользователь бота заполняет форму и нажимает кнопку для отправки формы.

  5. В коде HTML формы размещается JS код, для передачи введенных данных в телеграмм: window.Telegram.WebApp.sendData(JSON.stringify(formData)).
  6. Для дополнительной защиты, желательно, в отправляемые данные включить уникальный хэш-код лида.
  7. Metabot принимает данные заполненной формы от Telegram и сохраняет эти данные.
  8. Страница с формой в браузере будет закрыта автоматически, после выполнения метода Telegram.WebApp.sendData, если же вы не используете sendData, то можно вызвать метод Telegram.WebApp.close().

Ограничения и неудобства данного вида формы:

image.png

image.png

Web-формы в чат-боте и Web Apps

Создание HTML формы.

Инструкция для разработчика веб-формы.

Актуальный исходный код веб-формы реализующий все три вида форм смотрите по ссылке: go-to-the-mars.html

Пример работы веб-формы приведенной выше смотрите в Telegram боте https://t.me/metabot_test_form_bot

Исходный код примера веб-формы
<!DOCTYPE html>
<html lang="ru">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  <title>Заявка для полета на Марс</title>

  <!-- Подключаем Telegram Web App -->
  <script src="https://telegram.org/js/telegram-web-app.js"></script>

  <!-- Подключаем JQuery и JQ suggestions для dadata -->
  <!-- PS: JQuery подключать не обязательно, у вас могут быть свои библиотеки для работы с формой и отпарвки ajax запросов -->
  <script src="https://code.jquery.com/jquery-3.6.0.min.js"
          integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
  <script src="https://app.metabot24.com/lib/jquery.suggestions/js/jquery.suggestions.min.js"></script>
  <link href="https://app.metabot24.com/lib/jquery.suggestions/css/suggestions.min.css" rel="stylesheet"/>

  <script language="JavaScript">
    let botId = ID_ВАШЕГО_БОТА
    let botToken = 'ТОКЕН_ВАШЕГО_БОТА'
    let dadataToken = 'ТОКЕН_DADATA'

    // URL для POST запроса на который будет отправлять данные с формы
    // В качестве примера мы отправляем напрямую в бота
    // Но в вашем production боте вы должны отправлять на данные на бэк
    // А с бэка уже пересылать в бота, чтобы, например "не светить" токен бота в коде html-формы
    let sendUrl = '/api/v1/bots/' + botId + '/call/submit-form'

    let sendBody = {}
    let sendHeaders = {}
    let mode = null
    let tgWebApp = null

    $(function () {
      /* Определяем режим работы формы

         Режим передается в query url (GET параметр mode=)

         Режимы текущей html:
          - '' - пустая стркоа или null, когда в чат-боте ссылка на форму приходит в виде ссылки
                 это универсальный вариант который будет работать в любом мессенджере
          - 'tg_inline' -  для Telegram чат-бота, когда ссылка на форму приходит в виде inline-кнопки
          - 'tg_keyboard' - для Telegram чат-бота, когда ссылка на форму приходит в виде keyboard-кнопки

          Для вашего бота может быть достаточно одного из режимов
          В качестве примера просто приведены 3 варианта, чтобы вы могли выбрать подходящий и понять разницу
       */
      if (typeof (urlParams["mode"]) === 'string') {
        mode = urlParams["mode"]
      }
      // Переменная для доступа к Telegram Web App, чтобы не писать везде window.Telegram.WebApp
      if (mode === 'tg_inline' || mode === 'tg_keyboard') {
        if (window.Telegram && window.Telegram.WebApp) {
          tgWebApp = window.Telegram.WebApp
        }
      }

      // Инициалиця Dadata для автокомплита поля с адресом
      $(".dadata-suggestion").suggestions({
        token: dadataToken,
        type: "ADDRESS"
      })

      // Получаем хэш-код лида из request url (GET параметр q=)
      $('#q').val(urlParams["q"])

      // Событие нажатия на кнопку "Отправить данные"
      $(document).on('click', '#submit-mars-form', (e) => {
        let form = $('#form-mars')
        if (!form[0].checkValidity()) {
          form[0].reportValidity()
          return
        }

        let formData = getFormData(form)

        formData['tg_query_id'] = ''
        formData['mode'] = mode

        if (mode === 'tg_inline') {
          if (tgWebApp && tgWebApp.initDataUnsafe && tgWebApp.initDataUnsafe.query_id) {
            formData['tg_query_id'] = tgWebApp.initDataUnsafe.query_id
          }
        }

        if (tgWebApp) {
          // https://core.telegram.org/bots/webapps#initializing-web-apps
          tgWebApp.expand() // необязательно
          tgWebApp.ready() // необязательно
        }

        // Для универсального режима или режима tg_inline
        if (mode !== 'tg_keyboard') {

          // Отправляем данные внутри script_request_params и указываем токен и др заголовки для Metabot API
          // Но в вашем production, здесь вы должны просто отправить данные на ваш бэк, а с бэка уже в Metabot API
          // отправлять необзяталеьно через JSON API, можно делать это просто через ACTION формы SUBMIT кнопку на форме
          //
          // Если вы реализуете универсальный режим и выполняете отправку через action forma(сабмит без REST API),
          // то ваша форма разрывается на два шага
          // - заполнение формы клиентом
          // - получаем данные на бэке
          //      и после приема данных рендерим опять HTML в коде которого размещаем JavaScript который закроет страницу (вызовет метод closeForm())
          // Поэтому проще данные на бэк отправить по REST API и сразу же закрыть форму
          // Именно такой вариант и реализован в данной HTML форме
          sendBody = {"script_request_params": formData} //form.serializeArray()
          sendHeaders = {
            "Authorization": "Bearer " + botToken,
            'Content-Type': 'application/json',
            'Accept': 'application/json',
          }

          // Отправка запроса с помощью библиотеки JQuery
          sendJQueryRequest('POST', sendUrl, sendBody, sendHeaders, function (response, isError, jqXHR, textStatus, errorThrown) {
            if (!isError) {
              // Запрос завершён. Здесь можно обрабатывать результат.
              //console.log(response)

              closeForm()
            } else {
              // Произошла ошибка
              alert("Ошибка обработки API запроса")

              console.log(jqXHR)
            }
          })

          // Отправка запроса без дополнительных библиотек (с помощью XHR)
          // Могут быть проблемы с отправкой, возможно нужна корректировка кода под ваш бэкенд вашего сайта
          /*sendXhrRequest('POST', sendUrl, sendBody, sendHeaders, function(xhr) {
              //https://developer.mozilla.org/ru/docs/Web/API/XMLHttpRequest/send#%D0%BF%D1%80%D0%B8%D0%BC%D0%B5%D1%80_get
              if(xhr.readyState == XMLHttpRequest.DONE && xhr.status == 200) {
                // Запрос завершён. Здесь можно обрабатывать результат.
                console.log(xhr)
                closeForm()
              } else {
                // Произошла ошибка
                alert("Ошибка обработки API запроса")
                console.log(xhr)
              }
          })*/
        } else {
          // Если это режим tg_keyboard

          // Внимание! Лимит строки для sendData - 4096 байт !
          // Поэтому такой режим менее универсален, хотя на первый взгляд проще и не требует бэкенда
          // Но в будущем могут возникнуть проблемы, если форма будет усложнена

          // Если необходимо вывести сообщение (например для отладки),
          //   тк обычные alert и console.log для telegram Web App не сработают
          //tgWebApp.showAlert('Все ок!')

          tgWebApp.sendData(JSON.stringify(formData));

          // Если не выполняем sendData, то закрываем форму сами
          //closeForm()
        }
      })
    })

    /**
     * Метод для закрытия формы
     */
    function closeForm() {
      if (mode === null || mode === '') {
        location.href = "tg://resolve?domain=metabot_test_form_bot"
        window.close()
      } else {
        tgWebApp.close()
      }
    }

    // https://stackoverflow.com/questions/11338774/serialize-form-data-to-json
    function getFormData($form) {
      var unindexed_array = $form.serializeArray();
      var indexed_array = {};

      $.map(unindexed_array, function (n, i) {
        indexed_array[n['name']] = n['value'];
      });

      return indexed_array;
    }

    //https://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript
    var urlParams = (function (a) {
      if (a == "") return {}
      var b = {}
      for (var i = 0; i < a.length; ++i) {
        var p = a[i].split('=', 2)
        if (p.length == 1)
          b[p[0]] = ""
        else
          b[p[0]] = decodeURIComponent(p[1].replace(/\+/g, " "))
      }
      return b
    })(window.location.search.substr(1).split('&'))

    // Отправка POST запроса по API с помощью JQuery
    // https://reqbin.com/code/javascript/wzp2hxwh/javascript-post-request-example
    function sendJQueryRequest(method, url, data, jsonHeaders, callback) {
      $.ajax({
        url: url,
        type: method,
        contentType: 'application/json; charset=utf-8',
        dataType: 'json',
        async: false,

        headers: jsonHeaders,
        data: JSON.stringify(data),

        success: function (response) {
          callback(response, false)
        },
        error: function (jqXHR, textStatus, errorThrown) {
          //alert("Произошла ошибка при обработке API запроса")
          callback(null, true, jqXHR, textStatus, errorThrown)
        }
      })
    }

    // Отправка POST запроса по API с помощью XHR
    // https://reqbin.com/code/javascript/wzp2hxwh/javascript-post-request-example
    function sendXhrRequest(method, url, data, jsonHeaders, callback) {
      let xhr = new XMLHttpRequest();
      xhr.open(method, url);

      xhr.setRequestHeader("Accept", "application/json")
      xhr.setRequestHeader("Content-Type", "application/json")

      if (typeof (jsonHeaders) != "undefined") {
        for (let key in jsonHeaders) {
          xhr.setRequestHeader(key, jsonHeaders[key])
        }
      }

      xhr.onload = () => console.log(xhr.responseText)

      xhr.onload = function () {
        // Запрос завершён. Здесь можно обрабатывать результат.
        callback(xhr)
      };

      //Вызывает функцию при смене состояния.
      //xhr.onreadystatechange = function() {
      //  callback(xhr)
      //}

      xhr.send(JSON.stringify(data))
    }
  </script>

  <!-- Стили формы, в вашей релизации будет свой блок кода, подлючаемый в виде css файла -->
  <style>
    body {
      background-color: #070619;
      /*width: 100%;
      height: 100%;*/
      font-size: 18px;
    }

    .main-container {
      width: 95%;
      height: 95%;
    }

    form {
      color: #fff;
      border: 1px solid silver;
      border-radius: 10px;
      padding: 10px;

      margin: 10px auto 0 auto;
      width: 95%;
      max-width: 500px;
      height: 100%;
    }

    form .form-title {
      font-size: 20px;
      text-align: center;
      font-weight: bold;
    }

    form .form-group {
      margin: 10px;
    }

    form .form-group input {
      font-size: 18px;
    }

    form .form-group input[type=checkbox] {
      width: 20px;
      height: 20px;
    }

    form .form-group input.dadata-suggestion {
      max-width: 75%;
    }

    form .form-group select {
      font-size: 18px;
    }

    .suggestions-suggestions {
      color: #000
    }

    form .form-button {
      text-align: center;
      margin-bottom: 10px;
    }

    form .send-button {
      font-size: 18px;
      border: 1px solid #fff;
      border-radius: 3px;
      background-color: #fff;
      padding: 5px;
      font-weight: bold;
      text-decoration: none;
    }

    form .form-button button {
      font-size: 18px;
      font-weight: bold;
    }

    .mars {
      position: absolute;
      left: calc(55vw);
      /*right: 0;*/
      top: 0;
      z-index: -10;
      opacity: 0.8;
    }
  </style>
</head>
<body>

<!-- HTML КОД формы  -->

<div class="main-container">
  <!--Для отправки с помощью submit укажите аттрибут action данной формы-->
  <form id="form-mars" autocomplete="off">
    <div class="form-title">Заявка для полета на Марс</div>

    <input type="hidden" id="q" name="q" value="">

    <div class="form-group">
      <label for="name">
        Ваше имя:
      </label>
      <div>
        <input type="text" name="name" id="name" placeholder="Юрий Гагарин" required autofocus>
      </div>
    </div>

    <div class="form-group">
      <label for="email">
        Почта:
      </label>
      <div>
        <input type="email" name="email" id="email" placeholder="yuri@gagarin.ru">
      </div>
    </div>

    <div class="form-group">
      <label for="age">
        Возраст:
      </label>
      <div>
        <input type="number" name="age" id="age" min=12 max=777 step=1>
      </div>
    </div>

    <div class="form-group">
      <label for="specialization">
        Профессия:
      </label>
      <div>
        <select name="specialization" id="specialization" required>
          <option value="engineer" selected>Инженер</option>
          <option value="scientist">Учёный</option>
          <option value="psychologist">Психолог</option>
          <option value="other">Другая</option>
        </select>
      </div>
    </div>

    <div class="form-group">
      <label for="address">
        Ваш адрес проживания:
      </label>
      <input class="dadata-suggestion" type="text" name="address" id="address" placeholder="" required>
    </div>

    <div class="form-group">
      <label for="is_qualified">
        Прошел курсы в Центре<br>подготовки космонавтов
        <input type="checkbox" name="is_qualified" id="is_qualified" value="1">
      </label>
    </div>

    <div class="form-group">
      <label for="has_experience">
        Я уже летал в космос (имею опыт)
        <input type="checkbox" name="has_experience" id="has_experience" value="1">
      </label>
    </div>

    <!-- Если нужна отправка фото -->
    <!--<div class="form-group">
      <label>
        Фото:
        <input type="file" accept="image/jpeg" name="photo" required>
      </label>
    </div>-->

    <div class="form-button">
      <!--<button type="submit">Отправить заявку</button>--> <!-- Для отправки с помощью submit формы -->
      <a class="send-button" id="submit-mars-form" href="#">Отправить заявку</a>
    </div>
  </form>

  <img class="mars" src="./mars1.gif"/>
</div>
</body>
</html>

При использовании данного примера замените в следующих строках данные на актуальные для вашего бота:

И измените код отправки заполненных данных, чтобы данные сначала отправлялись на ваш бэк, а затем с бэка в Metabot API. Также уберите отправку данных внутри параметра script_request_params, чтобы данные к вам на бэк отправлялись без этого параметра, но когда отправляете в Metabot API учитывайте эту особенность.

Речь идет про строку:

sendBody = {"script_request_params": formData}

Которую для вас нужно заменить на:

sendBody = formData

Также для понимания принципа работы изучите комментарии в HTML-коде.

Код не требует большого уровня владения HTML или JS для создания подобной формы, но заметим, что важной частью является UI/UX и может потребоваться профессиональный Frontend-разработчик, также необходимо учесть отработку всех исключительных ситуаций, например если произошел сбой API, о чем было упомянуто в теоретической части, во введении, заметим что выше приведен просто пример, все аспекты не проработаны досконально, тк это возможно только на конкретном рабочем примере для вашего бота.

Web-формы в чат-боте и Web Apps

Универсальная форма в виде ссылки (для любого мессенджера)

Инструкция для разработчика бота.

Актуальный исходный код веб-формы реализующий все три вида форм смотрите по ссылке: go-to-the-mars.html

Пример работы веб-формы приведенной выше смотрите в Telegram боте https://t.me/metabot_test_form_bot

Форма открывается в отдельной странице браузера. Как обычная страница web-браузера.

Для Telegram ссылку можно отправить в виде inline-кнопки, но это все равно будет не "Web App приложение", а обычная страница открытая в браузере.

Если в мессенджере открытие ссылок во внешнем браузере выключено то форма откроется в браузере мессенджера, но все равно работа с данным видом формы будет отличаться от формы в виде Web App для inline/keyboard кнопки. При этом пользователю все равно доступна возможность позволяющая открыть ссылку во внешнем браузере. После закрытия формы нет гарантий, что пользователь вернется в браузер (например для PC), поэтому желательно прислать пользователю уведомление в бота, чтобы он нажал на него и вернулся в бота.

Если вы планируете использовать формы только в Telegram, то можете перейти к разделу описывающему форму на основе Web App, открывающейся с помощью inline-кнопки.

Краткое описание принципа работы бота:

  1. В бот отправляется ссылка на форму (или inline-кнопка в виде ссылки). При нажатии на ссылку (или inline-кнопку в виде ссылки) открывается внешний браузер (или браузер встроенный в Telegram).
  2. После заполнения формы данные должны быть отправлены на сторонний бэк, а из бэка данные должны быть отправлены в Metabot API. Для понимания смотрите исходный код веб-формы.
  3. Данные пришедшие с формы сохраняются в JS Internal API Endpoint, который сохраняет данные и вызывает скрипт бота для продолжения беседы, этот скрипт работает с сохраненными данными формы, запрашивает подтверждения, что все введено верно, а также предлагает заполнить форму повторно.

Принцип работы, по шагам:

  1. Бот генерирует ссылку с уникальным хэш-кодом, привязанным к лиду.

  2. Бот отправляет эту ссылку в мессенджер в виде обычного сообщения.

  3. Пользователь бота нажимает на ссылку, открывается браузер с формой.

  4. Пользователь бота заполняет форму и нажимает кнопку для отправки формы.

  5. Форма отправляет данные на backend сайта, где размещена форма.
  6. Backend сайта отправляет данные формы в API Metabot, в данные должен быть включен уникальный хэш лида.
  7. Metabot принимает и сохраняет данные заполненной формы.
  8. Если все ок, то страница с формой в браузере должна быть закрыта, это делается с помощью простого JS кода (подробности рассматриваются на отдельной странице документации с описанием создания HTML-формы).

Таблицы для работы с формами

Чтобы рассматриваемый пример бота с формами работал, вам необходимо создать две кастомные таблицы.

Таблица: Хэш-коды лидов

Таблица необходима для хранения соответствий между лидом и хэшом для лида.

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

Структура таблицы:

image.pngimage.png


Таблица: Данные форм

Таблица необходима для хранения данных заполненных форм.

Данная таблица реализована как универсальная, все данные пришедшие с формы сохраняются в текстовом поле в виде JSON, поэтому она может работать с любой формой, но работать с строкой в виде JSON в JS неудобно, тк для доступа к данным приходится выполнять JSON.parse(), вы можете нормализовать данные, реализуя для каждой формы бота отдельную таблицу с необходимыми полями для хранения, например такими как: Имя, Фамилия, Адрес и т.д., чтобы в дальнейшем было проще работать с данными и выполнять поиск по таблице. Также, в вашем случае таблица для хранения данных может и не потребоваться, если всплывающая форма очень простая и предназначена, например, для заполнения одного поля, например, адреса с авто комплитом от Dadata или для выбора выпадающего списка или даты в календаре. Т.е. форма может реализовывать даже один компонент для заполнения которого необходим HTML + JS.

Структура таблицы:

PKCu1KBiYK3GrQC6-image.png

image.png

Чтобы посмотреть исходный код внутреннего API, сначала перейдите в бота по ссылке на любой из скриптов.

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

Если у вас нет доступа к указанному скрипту, то запросите шаблон бота в технической поддерже Metabot.

Скрипт для формирования и отправки ссылки в мессенджер

Пример скрипта:

image.png

Скрипт отправки ссылки состоит из следующих команд бота:

let md5 = require('Common.Utils.Md5')

let expireAt = new Date()
expireAt.setSeconds(expireAt.getSeconds() + 300)

// Todo: Можно вынести в сниппет
let hashItems = table.find('lead_form_hash', [], [
  ['form', '=', 'mars'],
  ['lead_id', '=', leadId],
])

let leadHash
if (hashItems.length > 0) {
  leadHash = hashItems[0].hash
} else {
  const salt = 'YourSuperSecretString' + (new Date()).getTime()
  leadHash = md5(salt + leadId)
  
  table.createItem('lead_form_hash', {
    'form': 'mars',
    'lead_id': leadId,
    'hash': leadHash,
    'expireAt': expireAt
  })
}

// Вы можете вынести эту ссылку в атрибуты бота
let url = 'https://app.metabot24.com/testWidgets/go-to-the-mars.html?q=' + leadHash

memory.setAttr('formUrl', url)

PS: Здесь же можно отправить ссылку в виде inline кнопки (не inline Web App, а обычную кнопку с ссылкой) 
реализовав это в виде  команды JS Callback
но такой вариант будет работать только в Telegram
поэтому чтобы данный вариант формы был универсальным отправляем ее просто в виде ссылки

Перейдите по ссылке ниже и заполните форму

{{ &$formUrl }}

Внутреннее api для приема данных с формы и сохранения их в таблицу

Это точка API, куда приходит запрос после заполнения формы.

image.png

JavaScript для вычисления API ответа (Response Body):

let requestParams = request.json
let formLeadId = null

if (!request.json || !request.json.q) {
  return {"result": false, "message": "Invalid Lead Hash"}
}

// Todo: Можно вынести в сниппет
let hashItems = table.find('lead_form_hash', [], [
  ['form', '=', 'mars'],
  ['hash', '=', request.json.q],
])

let found = 0

if (hashItems.length > 0) {
  formLeadId = hashItems[0].lead_id
  hashItems.forEach(hashItem => hashItem.delete())
}

if (formLeadId > 0) {
  // Удаляем предыдущие ответы
  // Todo: Можно вынести в сниппет
  oldResults = table.find('form_results', [], [
    ['form', '=', 'mars'],
    ['lead_id', '=', formLeadId],
  ])
  if (oldResults.length > 0) {
    oldResults.forEach(oldResult => oldResult.delete())
  }
  
  // Предварительно сохраняем данные в таблице
  table.createItem('form_results', {
    "form": "mars",
    "lead_id": formLeadId,
    "data": request.string
  })

  bot.scheduleScriptByCode('after_submit', formLeadId)
  
  //Для передачи данных в скрипт без предварительного сохранения в таблицу
  //bot.scheduleScriptByCode('after_submit', leadId, null, {"script_request_params": requestParams})
  return {"result": true}
} else {
  return {"result": false, "message": "Lead by hash is not found"}
}

Скрипт выполняемый после вызова API

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

Пример скрипта.

Скрипт отправки ссылки состоит из следующих команд бота и пунктов меню:

const menuHelper = require('Common.TelegramComponents.MenuHelper')

// --------------------------------------------------------------
// Удаляем кнопку с формой для Inline формы, если она выведена

if (menuHelper.hasLastTelegramMessageId()) {
  // Удаляем кнопку с ссылкой на форму
  menuHelper.removeInlineKeyboard()

  // Сбрасываем ID последнего сообщения
  menuHelper.clearLastTelegramMessageId()
}
// --------------------------------------------------------------

// --------------------------------------------------------------
// Для Inline кнопки c формой
// Удаляем кнопку с формой , если она выведена
// Удаляем здесь - тк после заполнения формы в JS-Callback мы уже не попадаем
// Тк прием данных с формы осуществляет через Internal Endpoiint
// Для keyboard кнопки ссылку на форму с кнопкой здесь удалять не нужно, 
// тк для keyboard кнопки мы возвращаемся в JS-Callbak после заполнении формы
if (menuHelper.hasLastTelegramMessageId()) {
  // Удаляем кнопку
  menuHelper.removeInlineKeyboard()

  // Сбрасываем ID последнего сообщения
  menuHelper.clearLastTelegramMessageId()
}
// --------------------------------------------------------------

// Если получаем данные напрямую, без таблицы form_results
//let data = request.json
  
// Если получаем данные из таблицы form_results
// Todo: Можно вынести в сниппет
let data = table.find('form_results', [], [
  ['form', '=', 'mars'],
  ['lead_id', '=', leadId]
])

if (data.length > 0 && typeof(data[0].data) === 'string' && data[0].data.length > 0) {
  data = JSON.parse(data[0].data)
} else {
  data = {}
  bot.sendText('Ошибка! Данные не найдены')
}

// Для определения какую кнопку показывать в меню ниже
// См условие аунктов меню текущего скрипта
memory.setAttr('mode', '')
if (typeof(data.mode) === 'string' && data.mode.length > 0) {
  memory.setAttr('mode', data.mode)
}


/*
// Сообщение о результате отработки формы, отправляется в бота
// Но проблема что система распознает сообщение как вебхук от лида
// Поэтому использовать можно только в JS-Callback и игнорировать вебхук вручную
// Или через регулярку маршрута, запуская отдельны скрипт
//
// Данное сообщение также автоматом закрывает web-view
// Но пробелм с закрытием нет, это выполняется в JS итак с помощью
// кода window.Telegram.WebApp.sendData(JSON.stringify(formData));
// или при выполнении window.Telegram.WebApp.close()
//
// Обычно используется для игры в web_view для вывода очков после победы или проигрыша
if (data.tg_query_id && data.tg_query_id.length > 0) {
  //https://core.telegram.org/bots/api#sentwebappmessage
  //https://core.telegram.org/bots/api#inlinequeryresultarticle
  bot.sendPayload('answerWebAppQuery', {
    "web_app_query_id": data.tg_query_id,
    "result": {
      "type": "article", 
      "id": "1", // Unique identifier for this result, 1-64 Bytes
      "title": "Форма заполнена", 
      //"description": "Description", 
      "input_message_content": {"message_text": "Спасибо! Данные сохранены!"}
    }
  })
}
*/

memory.setJsonAttr('data', data)
memory.setAttr('is_qualified', data.is_qualified ? 'Да' : 'Нет')
memory.setAttr('has_experience', data.has_experience ? 'Да' : 'Нет')
Ваши данные:
Имя: {{ &$$data.name }}
Email: {{ &$$data.email }}
Возраст: {{ &$$data.age }}
Профессия: {{ &$$data.specialization}}
Ваш адрес проживания: {{ &$$data.address}}
Прошел курсы в Центре подготовки космонавтов: {{ &$is_qualified }}
Я уже летал в космос (имею опыт): {{ &$has_experience}}

Все данные в виде JSON:
{{ &$$data }}
Все верно?

image.png

Условия кнопок необходимы для вызова скрипта соответствующего текущему формату формы.

Условие для вывода универсальной формы в виде ссылки:

let mode = memory.getAttr('mode')
if (mode === 'tg_inline') {
  return true
}

Условие для вывода формы в Web App при нажатии на inline-кнопку:

let mode = memory.getAttr('mode')
if (mode !== 'tg_inline' && mode !== 'tg_keyboard') {
  return true
}

Условие для вывода формы в Web App при нажатии на keyboard-кнопку:

let mode = memory.getAttr('mode')
if (mode === 'tg_keyboard') {
  return true
}
Web-формы в чат-боте и Web Apps

Web App форма c использованием inline-кнопки (только для Telegram)

Инструкция для разработчика бота.

Данный вид формы работает только в Telegram.

Рекомендуемый вид формы для Telegram бота.

Актуальный исходный код веб-формы реализующий все три вида форм смотрите по ссылке: go-to-the-mars.html

Пример работы веб-формы приведенной выше смотрите в Telegram боте https://t.me/metabot_test_form_bot

Отличие данного варианта от универсальной формы в виде ссылки, в том, что форма работает как Web App, т.е. это специальный режиме браузера встроенного в Telegram. Этот режим гарантирует возврат в бота после заполнения формы, а также не позволит открыть ссылку во внешнем браузере (а также включает дополнительный функционал для взаимодействия веб-страницы с ботом). В обычном же режиме для универсальной формы, нет гарантий, что когда веб-страница будет закрыта, пользователя перекинет в бота.

Краткое описание принципа работы бота:

  1. В бот отправляется inline-кнопка (в формате Telegram Web App). При нажатии на кнопку открывается всплывающее окно, в которое встроено Web View, пользователь не может открыть эту ссылку во внешнем браузере, а также просмотреть исходный код формы (если только не откроет сам Telegram в браузере).
  2. После заполнения формы данные должны быть отправлены на сторонний бэк, а из бэка данные должны быть отправлены в Metabot API. Для понимания смотрите исходный код веб-формы.
  3. Данные пришедшие с формы сохраняются в JS Internal API Endpoint, который сохраняет данные и вызывает скрипт бота для продолжения беседы, этот скрипт работает с сохраненными данными формы, запрашивает подтверждения, что все введено верно, а также предлагает заполнить форму повторно.

Принцип работы, по шагам:

  1. Бот генерирует ссылку с уникальным хэш-кодом, привязанным к лиду.

  2. Бот отправляет эту ссылку в виде inline-кнопки в Telegram-мессенджер.

  3. Пользователь бота нажимает на кнопку, открывается Web App с формой (т.е. пользователь остается в Telegram, в мессенджере открывается встроенный браузер).

  4. Пользователь бота заполняет форму и нажимает кнопку для отправки формы.

  5. Форма отправляет данные на backend сайта, где размещена форма.
  6. Backend сайта отправляет данные формы в API Metabot, в данные должен быть включен уникальный хэш лида.
  7. Metabot принимает и сохраняет данные заполненной формы.
  8. Если все ок, то страница с формой в браузере должна быть закрыта, это делается с помощью простого JS кода (подробности рассматриваются на отдельной странице документации с описанием создания HTML-формы).

Таблицы для работы с формами

Чтобы рассматриваемый пример бота с формами работал, вам необходимо создать две кастомные таблицы.

Таблица: Хэш-коды лидов

Таблица необходима для хранения соответствий между лидом и хэшом для лида.

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

Структура таблицы:

image.pngimage.png


Таблица: Данные форм

Таблица необходима для хранения данных заполненных форм.

Данная таблица реализована как универсальная, все данные пришедшие с формы сохраняются в текстовом поле в виде JSON, поэтому она может работать с любой формой, но работать с строкой в виде JSON в JS неудобно, тк для доступа к данным приходится выполнять JSON.parse(), вы можете нормализовать данные, реализуя для каждой формы бота отдельную таблицу с необходимыми полями для хранения, например такими как: Имя, Фамилия, Адрес и т.д., чтобы в дальнейшем было проще работать с данными и выполнять поиск по таблице. Также, в вашем случае таблица для хранения данных может и не потребоваться, если всплывающая форма очень простая и предназначена, например, для заполнения одного поля, например, адреса с авто комплитом от Dadata или для выбора выпадающего списка или даты в календаре. Т.е. форма может реализовывать даже один компонент для заполнения которого необходим HTML + JS.

Структура таблицы:

PKCu1KBiYK3GrQC6-image.png

image.png

Чтобы посмотреть исходный код внутреннего API, сначала перейдите в бота по ссылке на любой из скриптов.

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

Если у вас нет доступа к указанному скрипту, то запросите шаблон бота в технической поддерже Metabot.

Скрипт для формирования ссылки и отправки кнопки в мессенджер

Пример скрипта.

Скрипт отправки ссылки состоит из единственной команды бота:

image.png

const menuHelper = require('Common.TelegramComponents.MenuHelper')
let md5 = require('Common.Utils.Md5')

// ---------------------------------------------------------------------------------------------------
// Инициализация компонента
if (menuHelper.isFirstImmediateCall()) {
  // !!! ИНИЦИАЛИЗАЦИЯ !!!

  // Скрываем предыдущее меню c кнопкой формы
  menuHelper.removeInlineKeyboard()

  // Сбрасываем ID последнего сообщения
  menuHelper.clearLastTelegramMessageId()
  
  // --------------------------
  //ГЕНЕРИРУМ ССЫЛКУ С ХЭШЕМ
  let expireAt = new Date()
  expireAt.setSeconds(expireAt.getSeconds() + 300)

  let hashItems = table.find('lead_form_hash', [], [
    ['form', '=', 'mars'],
    ['lead_id', '=', leadId],
  ])

  // Todo: Можно вынести в сниппет
  let leadHash
  if (hashItems.length > 0) {
    leadHash = hashItems[0].hash
  } else {
    const salt = 'YourSuperSecretString' + (new Date()).getTime()
    leadHash = md5(salt + leadId)

    table.createItem('lead_form_hash', {
      'form': 'mars',
      'lead_id': leadId,
      'hash': leadHash,
      'expireAt': expireAt
    })
  }

  // Вы можете вынести эту ссылку в атрибуты бота
  let formUrl = 'https://app.metabot24.com/testWidgets/go-to-the-mars.html?mode=tg_inline&q=' + leadHash
  // ----------------------------

  // Отправляем кнопку с формой при инициализации
  let message = 'Для продолжения, пожалуйста заполните форму'
  let buttons = [
    [
      /*{
        "text": 'Отмена',
        "callback_data": "btn_static_cancel",
      },*/
      {
        "text": 'Заполнить форму',
        //"callback_data": "btn_static_web_app",
        "web_app": {
          "url": formUrl
        },
      }
    ]
  ]
  let apiAdditionalParams = {
    "reply_markup": {
      "inline_keyboard": buttons
    }
  }
  menuHelper.sendMessage(message, null, null, apiAdditionalParams)
  
  return false
}
// ---------------------------------------------------------------------------------------------------

// ---------------------------------------------------------------------------------------------------
// ЛОГИКА КОМПОНЕНТА
// ---------------------------------------------------------------------------------------------------

// ---------------------------------------------------------------------------------------------------
// Обработка нажатых кнопок или входящего текста

if (["btn_static_cancel", "0"].includes(bot.getIncomingMessage().toLowerCase())) {
  // Скрываем предыдущее меню c кнопкой формы
  menuHelper.removeInlineKeyboard()

  // Или удаляем предыдущее сообщение, если необходимо
  // Не забываем выполнять аналогичное удаление при выходе из компонента с помощью маршрута
  //this.deletePrevMessage()

  // Или останавливаем анимацию ожидания на кнопке
  //menuHelper.answerCallbackQuery()    
  
  // Выходим из команды
  return {
    "break": true,
    //"run_script_by_code": "код_запускаемого_скрипта",
  }
} else {
  // Если получаем любое другое текстовое сообщение
  if (bot.getIncomingMessage() !== "") {
    bot.sendText('Пожалуйста заполните форму')
    
    return false
    
    // Чтобы вопрос не уходил вверх, тогда после удаления необходимо повторить вопрос
    //this.deletePrevMessage()    
    
    // Повторяем вопрос, если удаляем пред. вопрос строкой выше с помощью deletePrevMessage
    // ...
  } //else if(1 === 2) {
    // Данные отправленные из webview обрабатываем не здесь, а в Internal API Endpint
    //return true
  //}
}
// ---------------------------------------------------------------------------------------------------

Внутреннее api для приема данных с формы и сохранения их в таблицу

Это точка API, куда приходит запрос после заполнения формы.

image.png

JavaScript для вычисления API ответа (Response Body):

let requestParams = request.json
let formLeadId = null

if (!request.json || !request.json.q) {
  return {"result": false, "message": "Invalid Lead Hash"}
}

// Todo: Можно вынести в сниппет
let hashItems = table.find('lead_form_hash', [], [
  ['form', '=', 'mars'],
  ['hash', '=', request.json.q],
])

let found = 0

if (hashItems.length > 0) {
  formLeadId = hashItems[0].lead_id
  hashItems.forEach(hashItem => hashItem.delete())
}

if (formLeadId > 0) {
  // Удаляем предыдущие ответы
  // Todo: Можно вынести в сниппет
  oldResults = table.find('form_results', [], [
    ['form', '=', 'mars'],
    ['lead_id', '=', formLeadId],
  ])
  if (oldResults.length > 0) {
    oldResults.forEach(oldResult => oldResult.delete())
  }
  
  // Предварительно сохраняем данные в таблице
  table.createItem('form_results', {
    "form": "mars",
    "lead_id": formLeadId,
    "data": request.string
  })

  bot.scheduleScriptByCode('after_submit', formLeadId)
  
  //Для передачи данных в скрипт без предварительного сохранения в таблицу
  //bot.scheduleScriptByCode('after_submit', leadId, null, {"script_request_params": requestParams})
  return {"result": true}
} else {
  return {"result": false, "message": "Lead by hash is not found"}
}

Скрипт выполняемый после вызова API

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

Пример скрипта.

Скрипт отправки ссылки состоит из следующих команд бота и пунктов меню:

const menuHelper = require('Common.TelegramComponents.MenuHelper')

// --------------------------------------------------------------
// Удаляем кнопку с формой для Inline формы, если она выведена

if (menuHelper.hasLastTelegramMessageId()) {
  // Удаляем кнопку с ссылкой на форму
  menuHelper.removeInlineKeyboard()

  // Сбрасываем ID последнего сообщения
  menuHelper.clearLastTelegramMessageId()
}
// --------------------------------------------------------------

// --------------------------------------------------------------
// Для Inline кнопки c формой
// Удаляем кнопку с формой , если она выведена
// Удаляем здесь - тк после заполнения формы в JS-Callback мы уже не попадаем
// Тк прием данных с формы осуществляет через Internal Endpoiint
// Для keyboard кнопки ссылку на форму с кнопкой здесь удалять не нужно, 
// тк для keyboard кнопки мы возвращаемся в JS-Callbak после заполнении формы
if (menuHelper.hasLastTelegramMessageId()) {
  // Удаляем кнопку
  menuHelper.removeInlineKeyboard()

  // Сбрасываем ID последнего сообщения
  menuHelper.clearLastTelegramMessageId()
}
// --------------------------------------------------------------

// Если получаем данные напрямую, без таблицы form_results
//let data = request.json
  
// Если получаем данные из таблицы form_results
// Todo: Можно вынести в сниппет
let data = table.find('form_results', [], [
  ['form', '=', 'mars'],
  ['lead_id', '=', leadId]
])

if (data.length > 0 && typeof(data[0].data) === 'string' && data[0].data.length > 0) {
  data = JSON.parse(data[0].data)
} else {
  data = {}
  bot.sendText('Ошибка! Данные не найдены')
}

// Для определения какую кнопку показывать в меню ниже
// См условие аунктов меню текущего скрипта
memory.setAttr('mode', '')
if (typeof(data.mode) === 'string' && data.mode.length > 0) {
  memory.setAttr('mode', data.mode)
}


/*
// Сообщение о результате отработки формы, отправляется в бота
// Но проблема что система распознает сообщение как вебхук от лида
// Поэтому использовать можно только в JS-Callback и игнорировать вебхук вручную
// Или через регулярку маршрута, запуская отдельны скрипт
//
// Данное сообщение также автоматом закрывает web-view
// Но пробелм с закрытием нет, это выполняется в JS итак с помощью
// кода window.Telegram.WebApp.sendData(JSON.stringify(formData));
// или при выполнении window.Telegram.WebApp.close()
//
// Обычно используется для игры в web_view для вывода очков после победы или проигрыша
if (data.tg_query_id && data.tg_query_id.length > 0) {
  //https://core.telegram.org/bots/api#sentwebappmessage
  //https://core.telegram.org/bots/api#inlinequeryresultarticle
  bot.sendPayload('answerWebAppQuery', {
    "web_app_query_id": data.tg_query_id,
    "result": {
      "type": "article", 
      "id": "1", // Unique identifier for this result, 1-64 Bytes
      "title": "Форма заполнена", 
      //"description": "Description", 
      "input_message_content": {"message_text": "Спасибо! Данные сохранены!"}
    }
  })
}
*/

memory.setJsonAttr('data', data)
memory.setAttr('is_qualified', data.is_qualified ? 'Да' : 'Нет')
memory.setAttr('has_experience', data.has_experience ? 'Да' : 'Нет')
Ваши данные:
Имя: {{ &$$data.name }}
Email: {{ &$$data.email }}
Возраст: {{ &$$data.age }}
Профессия: {{ &$$data.specialization}}
Ваш адрес проживания: {{ &$$data.address}}
Прошел курсы в Центре подготовки космонавтов: {{ &$is_qualified }}
Я уже летал в космос (имею опыт): {{ &$has_experience}}

Все данные в виде JSON:
{{ &$$data }}
Все верно?

image.png

Условия кнопок необходимы для вызова скрипта соответствующего текущему формату формы.

Условие для вывода универсальной формы в виде ссылки:

let mode = memory.getAttr('mode')
if (mode === 'tg_inline') {
  return true
}

Условие для вывода формы в Web App при нажатии на inline-кнопку:

let mode = memory.getAttr('mode')
if (mode !== 'tg_inline' && mode !== 'tg_keyboard') {
  return true
}

Условие для вывода формы в Web App при нажатии на keyboard-кнопку:

let mode = memory.getAttr('mode')
if (mode === 'tg_keyboard') {
  return true
}
Web-формы в чат-боте и Web Apps

Web App форма c использованием keyboard-кнопки (только для Telegram)

Инструкция для разработчика бота.

Данный вид формы работает только в Telegram.

Ключевая особенность: форма открывается в всплывающем окне на котором расположен WebView (встроенный браузер). Для отправки данных в Metabot API не требуется дополнительный backend.

Актуальный исходный код веб-формы реализующий все три вида форм смотрите по ссылке: go-to-the-mars.html

Пример работы веб-формы приведенной выше смотрите в Telegram боте https://t.me/metabot_test_form_bot

Отличие данного варианта от универсальной формы в виде ссылки, в том, что форма работает как Web App, т.е. это специальный режиме браузера встроенного в Telegram. Этот режим гарантирует возврат в бота после заполнения формы, а также не позволит открыть ссылку во внешнем браузере (а также включает дополнительный функционал для взаимодействия веб-страницы с ботом). В обычном же режиме для универсальной формы, нет гарантий, что когда веб-страница будет закрыта, пользователя перекинет в бота.

Краткое описание принципа работы бота:

  1.  В бот отправляется keyboard-кнопка (в формате Telegram Web App). При нажатии на кнопку открывается всплывающее окно, в которое встроен Web View, пользователь не может открыть эту ссылку во внешнем браузере, а также просмотреть исходный код формы (если только не откроет сам Telegram в браузере).
  2.  После заполнения формы и нажатия на кнопку Отправить данные, все данные будут отправлены в Metabot, в команду JavaSctript Callback, Internal API endpoint в данном случае не требуется. Для понимания смотрите исходный код веб-формы.
  3. Данные пришедшие с формы доступны в команде JavaSctript Callback в виде обычного вебхука. Необходимо сохранить их в таблице.
  4. Далее  необходимо вызвать скрипт бота для продолжения беседы, этот скрипт работает с сохраненными данными формы, запрашивает подтверждение, что все введено верно, а также предлагает заполнить форму повторно.

Принцип работы, по шагам:

Таблицы для работы с формами

Чтобы рассматриваемый пример бота с формами работал, вам необходимо создать две кастомные таблицы.

Таблица: Хэш-коды лидов

Таблица необходима для хранения соответствий между лидом и хэшом для лида.

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

Структура таблицы:

image.pngimage.png

Таблица: Данные форм

Таблица необходима для хранения данных заполненных форм.

Данная таблица реализована как универсальная, все данные пришедшие с формы сохраняются в текстовом поле в виде JSON, поэтому она может работать с любой формой, но работать с строкой в виде JSON в JS неудобно, тк для доступа к данным приходится выполнять JSON.parse(), вы можете нормализовать данные, реализуя для каждой формы бота отдельную таблицу с необходимыми полями для хранения, например такими как: Имя, Фамилия, Адрес и т.д., чтобы в дальнейшем было проще работать с данными и выполнять поиск по таблице. Также, в вашем случае таблица для хранения данных может и не потребоваться, если всплывающая форма очень простая и предназначена, например, для заполнения одного поля, например, адреса с авто комплитом от Dadata или для выбора выпадающего списка или даты в календаре. Т.е. форма может реализовывать даже один компонент для заполнения которого необходим HTML + JS.

Структура таблицы:

PKCu1KBiYK3GrQC6-image.png

image.png

Чтобы посмотреть исходный код внутреннего API, сначала перейдите в бота по ссылке на любой из скриптов.

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

Если у вас нет доступа к указанному скрипту, то запросите шаблон бота в технической поддерже Metabot.

Скрипт для формирования ссылки и отправки кнопки в мессенджер

Пример скрипта.

Скрипт отправки ссылки состоит из единственной команды бота:

image.png

const menuHelper = require('Common.TelegramComponents.MenuHelper')
let md5 = require('Common.Utils.Md5')

// ---------------------------------------------------------------------------------------------------
// Инициализация компонента
if (menuHelper.isFirstImmediateCall()) {
  // !!! ИНИЦИАЛИЗАЦИЯ !!!

  // Скрываем предыдущее меню c кнопкой формы
  menuHelper.removeInlineKeyboard()

  // Сбрасываем ID последнего сообщения
  menuHelper.clearLastTelegramMessageId()
  
  // --------------------------
  //ГЕНЕРИРУМ ССЫЛКУ С ХЭШЕМ
  let expireAt = new Date()
  expireAt.setSeconds(expireAt.getSeconds() + 300)

  let hashItems = table.find('lead_form_hash', [], [
    ['form', '=', 'mars'],
    ['lead_id', '=', leadId],
  ])

  // Todo: Можно вынести в сниппет
  let leadHash
  if (hashItems.length > 0) {
    leadHash = hashItems[0].hash
  } else {
    const salt = 'YourSuperSecretString' + (new Date()).getTime()
    leadHash = md5(salt + leadId)

    table.createItem('lead_form_hash', {
      'form': 'mars',
      'lead_id': leadId,
      'hash': leadHash,
      'expireAt': expireAt
    })
  }

  // Вы можете вынести эту ссылку в атрибуты бота
  let formUrl = 'https://app.metabot24.com/testWidgets/go-to-the-mars.html?mode=tg_keyboard&q=' + leadHash
  // ----------------------------

  // Отправляем кнопку с формой при инициализации
  let message = 'Для продолжения, пожалуйста заполните форму'
  let buttons = [
    [
      {
        "text": "Заполнить форму",
        "web_app": {
          "url": formUrl
        }
      }
    ]
  ]
  let apiAdditionalParams = {
    "reply_markup": {
      "keyboard": buttons, 
      "resize_keyboard": true // Для того чтобы кнопка не была слишком большой по высоте
    }
  }
  menuHelper.sendMessage(message, null, null, apiAdditionalParams)
  
  return false
}
// ---------------------------------------------------------------------------------------------------

// ---------------------------------------------------------------------------------------------------
// ЛОГИКА КОМПОНЕНТА
// ---------------------------------------------------------------------------------------------------

// ---------------------------------------------------------------------------------------------------
// Обработка нажатых кнопок или входящего текста

let webhook = bot.getWebhookPayload() 
let webAppData = null
let data = null
let rawJsonData = null

if (typeof webhook.message == 'object') {
  if (typeof webhook.message.web_app_data == 'object') {
    webAppData = webhook.message.web_app_data
    
    //Example:
    //"web_app_data": {
    //  "button_text": "Заполнить форму",
    //    "data": "{...}"
    //}
    
    if (typeof webAppData.data === 'string') {
      rawJsonData = webAppData.data
      data = JSON.parse(rawJsonData)
      if (typeof data !== 'object') {
        rawJsonData = null
        data = null
      }
    }
  }
}

// ---------------------------------
if (data !== null && typeof data === 'object' && typeof data.q === 'string') {
  // СОХРАНЕНИЕ ДАННЫХ
  // КОД ИДЕНТИЧНЫЙ API INTERNAL ENDPOINT
  // TODO: МОЖНО ВЫНЕСТИ В СНИППЕТ
  
  // Доп защита
  // Все равно проверяем хэш (на случай чтобы не взломали)
  // А также чтобы почистить формы ожидающие заполнения по лиду (хэши по лиду)
  // Можно не выполнять эту проверку здесь
  // Todo: Можно вынести в сниппет
  let hashItems = table.find('lead_form_hash', [], [
    ['form', '=', 'mars'],
    ['hash', '=', data.q],
  ])

  let found = 0

  if (hashItems.length > 0) {
    formLeadId = hashItems[0].lead_id
    hashItems.forEach(hashItem => hashItem.delete())
  }

  // Доп защита
  // Проверяем что к нам пришла форма принадлежащая лиду
  // Можно не выполнять эту проверку здесь
  if (formLeadId === leadId) {
    // Удаляем предыдущие ответы
    // Todo: Можно вынести в сниппет
    oldResults = table.find('form_results', [], [
      ['form', '=', 'mars'],
      ['lead_id', '=', formLeadId],
    ])
    if (oldResults.length > 0) {
      oldResults.forEach(oldResult => oldResult.delete())
    }

    // Предварительно сохраняем данные в таблице
    table.createItem('form_results', {
      "form": "mars",
      "lead_id": formLeadId,
      "data": rawJsonData
    })

    // Удаляем кнопку калькулятора, если она есть !
    // Внимание! Удалять можно только с отправкой сообщения!
    menuHelper.removeReplyKeyboardWithMessage('Спасибо, данные получены!')
    
    //bot.scheduleScriptByCode('after_submit', formLeadId)
    
    // Выходим из цикла и запускаем скрипт уведомления о принятой форме
    return {
      "break": true,
      "run_script_by_code": "after_submit"
    }
  } else {
    // Можно вывести сообщение - что чтото прилетело из того что не ожидали 
    // или выслать письмо администратору
    // ...
  }
}
// ---------------------------------

// Todo: Здесь можно повторно отправить кнопку, тк она скрывается с экрана если отправить сообщение в бота
bot.sendText('Пожалуйста, заполните форму, для этого нажмите на кнопку в нижней части мессенджера,'
             + ' после этого откроется форма, которую вам необходимо заполнить'
             + ' и нажать кнопку на форме "Отправить заявку"')

return false

// ---------------------------------------------------------------------------------------------------

Обратите внимание на строки с кодом запуска скрипта, после сохранения данных с формы:

    // Выходим из цикла и запускаем скрипт уведомления о принятой форме
    return {
      "break": true,
      "run_script_by_code": "after_submit"
    }

Этот код может быть заменен на "return true" для выхода из цикла и на команду бота - "Выполнить скрипт", которую в таком случае необходимо разместить после команды JavaScript Callback. Подробнее смотрите в описании команды Выполнить JavaScript Callback.

Скрипт выполняемый после сохранения данных 

Данный скрипт запускается из JavaScript Callback, код которой приведен выше.

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

Пример скрипта.

Скрипт отправки ссылки состоит из следующих команд бота и пунктов меню:

const menuHelper = require('Common.TelegramComponents.MenuHelper')

// --------------------------------------------------------------
// Удаляем кнопку с формой для Inline формы, если она выведена

if (menuHelper.hasLastTelegramMessageId()) {
  // Удаляем кнопку с ссылкой на форму
  menuHelper.removeInlineKeyboard()

  // Сбрасываем ID последнего сообщения
  menuHelper.clearLastTelegramMessageId()
}
// --------------------------------------------------------------

// --------------------------------------------------------------
// Для Inline кнопки c формой
// Удаляем кнопку с формой , если она выведена
// Удаляем здесь - тк после заполнения формы в JS-Callback мы уже не попадаем
// Тк прием данных с формы осуществляет через Internal Endpoiint
// Для keyboard кнопки ссылку на форму с кнопкой здесь удалять не нужно, 
// тк для keyboard кнопки мы возвращаемся в JS-Callbak после заполнении формы
if (menuHelper.hasLastTelegramMessageId()) {
  // Удаляем кнопку
  menuHelper.removeInlineKeyboard()

  // Сбрасываем ID последнего сообщения
  menuHelper.clearLastTelegramMessageId()
}
// --------------------------------------------------------------

// Если получаем данные напрямую, без таблицы form_results
//let data = request.json
  
// Если получаем данные из таблицы form_results
// Todo: Можно вынести в сниппет
let data = table.find('form_results', [], [
  ['form', '=', 'mars'],
  ['lead_id', '=', leadId]
])

if (data.length > 0 && typeof(data[0].data) === 'string' && data[0].data.length > 0) {
  data = JSON.parse(data[0].data)
} else {
  data = {}
  bot.sendText('Ошибка! Данные не найдены')
}

// Для определения какую кнопку показывать в меню ниже
// См условие аунктов меню текущего скрипта
memory.setAttr('mode', '')
if (typeof(data.mode) === 'string' && data.mode.length > 0) {
  memory.setAttr('mode', data.mode)
}


/*
// Сообщение о результате отработки формы, отправляется в бота
// Но проблема что система распознает сообщение как вебхук от лида
// Поэтому использовать можно только в JS-Callback и игнорировать вебхук вручную
// Или через регулярку маршрута, запуская отдельны скрипт
//
// Данное сообщение также автоматом закрывает web-view
// Но пробелм с закрытием нет, это выполняется в JS итак с помощью
// кода window.Telegram.WebApp.sendData(JSON.stringify(formData));
// или при выполнении window.Telegram.WebApp.close()
//
// Обычно используется для игры в web_view для вывода очков после победы или проигрыша
if (data.tg_query_id && data.tg_query_id.length > 0) {
  //https://core.telegram.org/bots/api#sentwebappmessage
  //https://core.telegram.org/bots/api#inlinequeryresultarticle
  bot.sendPayload('answerWebAppQuery', {
    "web_app_query_id": data.tg_query_id,
    "result": {
      "type": "article", 
      "id": "1", // Unique identifier for this result, 1-64 Bytes
      "title": "Форма заполнена", 
      //"description": "Description", 
      "input_message_content": {"message_text": "Спасибо! Данные сохранены!"}
    }
  })
}
*/

memory.setJsonAttr('data', data)
memory.setAttr('is_qualified', data.is_qualified ? 'Да' : 'Нет')
memory.setAttr('has_experience', data.has_experience ? 'Да' : 'Нет')
Ваши данные:
Имя: {{ &$$data.name }}
Email: {{ &$$data.email }}
Возраст: {{ &$$data.age }}
Профессия: {{ &$$data.specialization}}
Ваш адрес проживания: {{ &$$data.address}}
Прошел курсы в Центре подготовки космонавтов: {{ &$is_qualified }}
Я уже летал в космос (имею опыт): {{ &$has_experience}}

Все данные в виде JSON:
{{ &$$data }}
Все верно?

image.png

Условия кнопок необходимы для вызова скрипта соответствующего текущему формату формы.

Условие для вывода универсальной формы в виде ссылки:

let mode = memory.getAttr('mode')
if (mode === 'tg_inline') {
  return true
}

Условие для вывода формы в Web App при нажатии на inline-кнопку:

let mode = memory.getAttr('mode')
if (mode !== 'tg_inline' && mode !== 'tg_keyboard') {
  return true
}

Условие для вывода формы в Web App при нажатии на keyboard-кнопку:

let mode = memory.getAttr('mode')
if (mode === 'tg_keyboard') {
  return true
}

Типовые проблемы

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

1. Некоторые популярные продукты, такие как Mindbox или Airtable, отказываются авторизовывать запросы, если параметр Authorization в заголовке запроса записан с большой буквы. Странно, но факт.

Советы и Трюки

Данный раздел создан для разработчиков, стремящихся упростить и ускорить свою работу, избегая рутинных задач. Здесь собраны практические советы и элегантные решения, которые помогут вам писать меньше кода, сделать вашу работу более эффективной и избавить от необходимости «изобретать велосипед». Вы найдете здесь идеи для оптимизации разработки, примеры умных подходов к решению стандартных задач и творческие приемы, которые сделают ваш код более лаконичным и мощным. Этот раздел призван повысить вашу личную производительность, помочь сосредоточиться на творческой и значимой части работы, минимизируя рутинные и повторяющиеся процессы.

Советы и Трюки

Использование оператора ?? для проверки доступа и сокращения количества кода

Контекст

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

Проблема

Традиционные подходы к решению этой задачи могут быть громоздкими и требовать написания обширного кода для проверки наличия и значений определенных ключей в объектах JSON. Это увеличивает сложность кода и его объем.

Решение: Использование Оператора ??

Оператор ??, также известный как оператор нулевого слияния (nullish coalescing operator), предлагает элегантное решение для этой задачи. Он позволяет вернуть правую сторону выражения, если значение слева от ?? является null или undefined.

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

Пример Использования

В данном примере мы используем ?? для управления отображением пунктов меню в зависимости от прав доступа пользователя:

// Получение списка проектов с правом инициации голосований
let projects = get_projects_for_voting_initiation(member_id)
memory.setJsonAttr("projects", projects)

// Возвращает массив, ключ - ID проекта, а значение - право доступа: 
// {
//    "1": 0,
//    "2": 1
// }
Тогда в меню можно использовать следующий лаконичный синтаксис:

// Условия вывода пунктов меню
// Если projects[1] (Metabot Platform) равно true, показываем пункт меню
return memory.getJsonAttr("projects")[1] ?? false

// Если projects[2] (Metabot & Co.) равно true, показываем пункт меню
return memory.getJsonAttr("projects")[2] ?? false

Пример:

image.png

Преимущества

Разработка в паре с CoPilot AI

В этой главе рассматриваются ключевые аспекты и методики ведения разработки на платформе Metabot в сотрудничестве с искусственным интеллектом, например ChatGPT. Основное внимание уделяется эффективному взаимодействию между человеком-разработчиком и AI, целям и преимуществам такого подхода, а также техникам, которые помогают ускорить и упростить процесс разработки, поддерживая при этом высокое качество и точность работы. Эта глава будет особенно полезна тем, кто стремится оптимизировать свою производительность и исследовать инновационные методы работы с Low-Code платформами, используя возможности AI.

Разработка в паре с CoPilot AI

Case: Модификация кода с помощью CoPilot AI - Расширение доступа к результатам голосования

Этот кейс демонстрирует процесс модификации кода в Low-Code приложении Metabot для голосования с использованием ChatGPT4 в качестве AI-ассистента CoPilot.

Исходная задача заключается в расширении функционала приложения: ранее результаты голосования могли просматривать только владельцы и старшие партнеры, но теперь доступ к результатам голосования предоставляется всем участникам проекта, которые принимали участие в голосовании.

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

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

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

Prompt 1

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

var current_member_id = lead.getAttr("member_id");
var entered_voting_id = lead.getAttr("voting_id");

// Поиск голосования и его статуса
var voting = table.find("votings", ["id", "status_id", "project_id",
                                    "object_type", "object_id", "description",
                                    "start_date", "end_date", "url"],
                        [["id", "=", entered_voting_id]])[0];
if (!voting) {
  memory.setAttr("voting_result", "VOTING_NOT_FOUND");
  return;
}

// Добавление дополнительной информации о голосовании
var project_name = table.find("projects", ["name"], [["id", "=", voting.project_id]])[0]?.name;
var subject_type_name = table.find("subject_types", ["name"], [["code", "=", voting.object_type]])[0]?.name;
memory.setAttr("project_name", project_name);
memory.setAttr("subject_type_name", subject_type_name);
memory.setAttr("subject_id", voting.object_id || "Не указано");
memory.setAttr("description", voting.description);
memory.setAttr("start_date", voting.start_date);
memory.setAttr("end_date", voting.end_date);
memory.setAttr("url", voting.url);

var voting_status_id = voting.status_id;
var voting_status = table.find("voting_statuses", ["name"], [["id", "=", voting_status_id]])[0]?.name;
memory.setAttr("voting_status", voting_status);

/// Получение всех статусов пользователя в проекте
var user_statuses_records = table.find("project_member_statuses", ["status_id"], [
  ["project_id", "=", voting.project_id],
  ["member_id", "=", current_member_id]
]);

var user_statuses = user_statuses_records.map(function(record) { return record.status_id; });

// ID статусов, дающих доступ к результатам голосования
var access_status_ids = table.find("member_statuses", ["id"], [
  ["code", "IN", ["owner", "project_owner", "senior_partner"]]
]).map(function(record) { return record.id; }); // Преобразование результатов в массив ID

var hasAccess = user_statuses.some(function(status_id) {
  return access_status_ids.includes(status_id);
});

if (!hasAccess) {
  memory.setAttr("voting_result", "ACCESS_DENIED");
  return;
}

// Формирование таблицы результатов голосования
var votes = table.find("votes", ["member_id", "vote_type_id", "comment"], [["voting_id", "=", entered_voting_id]]);
var votesTable = "# | Участник | Голос | Комментарий\n" +
                "----------------------------------------------------------\n"

let n = 1
votes.forEach(vote => {
  var member_name = table.find("members", ["first_name", "last_name"], [["id", "=", vote.member_id]])[0];
  var vote_type = table.find("vote_types", ["name"], [["id", "=", vote.vote_type_id]])[0]?.name;
  var formatted_vote_type = {
    "За": "✅ За",
    "Против": "❌ Против",
    "Воздержался": "🤐 Воздержался",
    "Ожидается": "🕒 Ожидается"
  }[vote_type] || "🕒 Ожидается";
 
  votesTable += n++ + " | " + member_name.first_name + " " + member_name.last_name + " | " +
                formatted_vote_type + " | " +
                (vote.comment || "") + "\n";
});

memory.setAttr("voting_results_table", votesTable);

ChatGPT

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

var current_member_id = lead.getAttr("member_id");
var entered_voting_id = lead.getAttr("voting_id");

// Поиск голосования
var voting = table.find("votings", ["id", "status_id", "project_id", "object_type", "object_id", "description", "start_date", "end_date", "url"], [["id", "=", entered_voting_id]])[0];

if (!voting) {
    memory.setAttr("voting_result", "VOTING_NOT_FOUND");
    return;
}

// Проверка, участвовал ли пользователь в голосовании
var userVote = table.find("votes", ["id"], [["voting_id", "=", entered_voting_id], ["member_id", "=", current_member_id]]);

var hasAccess = userVote.length > 0;

if (!hasAccess) {
    memory.setAttr("voting_result", "ACCESS_DENIED");
    return;
}

// Формирование таблицы результатов голосования
var votes = table.find("votes", ["member_id", "vote_type_id", "comment"], [["voting_id", "=", entered_voting_id]]);
var votesTable = "# | Участник | Голос | Комментарий\n" +
                 "----------------------------------------------------------\n"

let n = 1;
votes.forEach(vote => {
    var member_name = table.find("members", ["first_name", "last_name"], [["id", "=", vote.member_id]])[0];
    var vote_type = table.find("vote_types", ["name"], [["id", "=", vote.vote_type_id]])[0]?.name;
    var formatted_vote_type = {
        "За": "✅ За",
        "Против": "❌ Против",
        "Воздержался": "🤐 Воздержался",
        "Ожидается": "🕒 Ожидается"
    }[vote_type] || "🕒 Ожидается";
  
    votesTable += n++ + " | " + member_name.first_name + " " + member_name.last_name + " | " +
                  formatted_vote_type + " | " +
                  (vote.comment || "") + "\n";
});

memory.setAttr("voting_results_table", votesTable);

В этой версии кода, проверка наличия доступа к результатам голосования основана на наличии записи участника в таблице votes для данного голосования. Если такая запись есть, пользователь имеет доступ к просмотру результатов голосования.

Комментарий оператора

Код верный, без ошибок — я проверил. Но GPT потерял часть кода, связанную с выводом карточки голосования — видимо он забыл об этом нюансе, ведь со-дизайн этого блока мы делали давно, а также он потерял переносы в длинной строке номер 5, но это я сам поправлю потом.

image.png

Исходный код этой части диалогового сценария:

image.png

Копируем эту часть скрипта с платформы Metabot в GPT и делаем новый запрос.

Prompt 2

Ты потерял блок с дополнительной инфой, для вывода контента, вот контент сценария:

Send Text    
return memory.getAttr("voting_result") == "VOTING_NOT_FOUND"
🚫 К сожалению, мы не смогли найти голосование с указанным ID. Пожалуйста, проверьте данные и попробуйте снова.
 
Send Text    
return memory.getAttr("voting_result") == "ACCESS_DENIED"
🔒 Извините, у вас нет доступа к результатам этого голосования. Доступ предоставляется определенным категориям участников проекта.
 
Send Text    
return memory.getAttr("voting_results_table")
🌟 Текущее Голосование в {{ &$project_name }}! 🌟

Мы рады видеть ваше участие в процессе принятия решений в рамках проекта {{ &$project_name }}. Вот ключевая информация о голосовании:

🔢 ID Голосования: {{ $voting_id }}
📜 Проект: '{{ &$project_name }}'
📄 Тема: {{ &$subject_type_name }}/{{ &$subject_id }}
📝 Описание: {{ &$description }}
🔗 Ссылка на Детали: {{ &$url }}🔗
📅 Дата Начала: '{{ &$start_date }}'
📅 Дата Окончания: '{{ &$end_date }}'
📌 Статус голосования: {{ &$voting_status }}

📊 Результаты:
{{ &$voting_results_table }}
Ваше участие помогает формировать будущее проекта!

ChatGPT

Понял вас, вот обновленный код с учетом блока дополнительной информации для вывода контента в сценарии:

var current_member_id = lead.getAttr("member_id");
var entered_voting_id = lead.getAttr("voting_id");

// Поиск голосования
var voting = table.find("votings", ["id", "status_id", "project_id", "object_type", "object_id", "description", "start_date", "end_date", "url"], [["id", "=", entered_voting_id]])[0];

if (!voting) {
    memory.setAttr("voting_result", "VOTING_NOT_FOUND");
    return;
}

// Дополнительная информация о голосовании
var project_name = table.find("projects", ["name"], [["id", "=", voting.project_id]])[0]?.name;
var subject_type_name = table.find("subject_types", ["name"], [["code", "=", voting.object_type]])[0]?.name;

memory.setAttr("project_name", project_name);
memory.setAttr("subject_type_name", subject_type_name);
memory.setAttr("subject_id", voting.object_id || "Не указано");
memory.setAttr("description", voting.description);
memory.setAttr("start_date", voting.start_date);
memory.setAttr("end_date", voting.end_date);
memory.setAttr("url", voting.url);

var voting_status_id = voting.status_id;
var voting_status = table.find("voting_statuses", ["name"], [["id", "=", voting_status_id]])[0]?.name;
memory.setAttr("voting_status", voting_status);

// Проверка участия пользователя в голосовании
var userVote = table.find("votes", ["id"], [["voting_id", "=", entered_voting_id], ["member_id", "=", current_member_id]]);

if (userVote.length === 0) {
    memory.setAttr("voting_result", "ACCESS_DENIED");
    return;
}

// Формирование таблицы результатов голосования
var votes = table.find("votes", ["member_id", "vote_type_id", "comment"], [["voting_id", "=", entered_voting_id]]);
var votesTable = "Участник | Голос | Комментарий\n" +
                 "----------------------------------------------------------\n";

votes.forEach(vote => {
    var member_name = table.find("members", ["first_name", "last_name"], [["id", "=", vote.member_id]])[0];
    var vote_type = table.find("vote_types", ["name"], [["id", "=", vote.vote_type_id]])[0]?.name;
    var formatted_vote_type = {
        "За": "✅ За",
        "Против": "❌ Против",
        "Воздержался": "🤐 Воздержался",
        "Ожидается": "🕒 Ожидается"
    }[vote_type] || "🕒 Ожидается";
  
    votesTable += member_name.first_name + " " + member_name.last_name + " | " +
                  formatted_vote_type + " | " +
                  (vote.comment || "Нет комментария") + "\n";
});

memory.setAttr("voting_results_table", votesTable);

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

Комментарий разработчика

Отлично! Код работает и написан как я хотел. Но, эта гадина потеряла нумерацию строк!

Prompt 3

Ты потерял нумерацию строк. Исправь - с нею лучше понятно сколько людей участвуют в голосовании.

ChatGPT

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

var current_member_id = lead.getAttr("member_id");
var entered_voting_id = lead.getAttr("voting_id");

// Поиск голосования
var voting = table.find("votings", ["id", "status_id", "project_id", 
                                    "object_type", "object_id", "description", 
                                    "start_date", "end_date", "url"], 
                        [["id", "=", entered_voting_id]])[0];

if (!voting) {
    memory.setAttr("voting_result", "VOTING_NOT_FOUND");
    return;
}

// Дополнительная информация о голосовании
var project_name = table.find("projects", ["name"], [["id", "=", voting.project_id]])[0]?.name;
var subject_type_name = table.find("subject_types", ["name"], [["code", "=", voting.object_type]])[0]?.name;

memory.setAttr("project_name", project_name);
memory.setAttr("subject_type_name", subject_type_name);
memory.setAttr("subject_id", voting.object_id || "Не указано");
memory.setAttr("description", voting.description);
memory.setAttr("start_date", voting.start_date);
memory.setAttr("end_date", voting.end_date);
memory.setAttr("url", voting.url);

var voting_status_id = voting.status_id;
var voting_status = table.find("voting_statuses", ["name"], [["id", "=", voting_status_id]])[0]?.name;
memory.setAttr("voting_status", voting_status);

// Проверка участия пользователя в голосовании
var userVote = table.find("votes", ["id"], [["voting_id", "=", entered_voting_id], ["member_id", "=", current_member_id]]);

if (userVote.length === 0) {
    memory.setAttr("voting_result", "ACCESS_DENIED");
    return;
}

// Формирование таблицы результатов голосования
var votes = table.find("votes", ["member_id", "vote_type_id", "comment"], [["voting_id", "=", entered_voting_id]]);
var votesTable = "# | Участник | Голос | Комментарий\n" +
                 "----------------------------------------------------------\n";

let n = 1;
votes.forEach(vote => {
    var member_name = table.find("members", ["first_name", "last_name"], [["id", "=", vote.member_id]])[0];
    var vote_type = table.find("vote_types", ["name"], [["id", "=", vote.vote_type_id]])[0]?.name;
    var formatted_vote_type = {
        "За": "✅ За",
        "Против": "❌ Против",
        "Воздержался": "🤐 Воздержался",
        "Ожидается": "🕒 Ожидается"
    }[vote_type] || "🕒 Ожидается";
  
    votesTable += n++ + " | " + member_name.first_name + " " + member_name.last_name + " | " +
                  formatted_vote_type + " | " +
                  (vote.comment || "Нет комментария") + "\n";
});

memory.setAttr("voting_results_table", votesTable);

Результат

image.png

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

Отзыв разработчика о работе с CoPilot на платформе Metabot:

ChatGPT успешно справился! Теперь чат-бот присылал карточку со всей информацией, как я и задумывал.

Я не замерял специально, но по моим субъективным ощущениям, он сэкономил мне, как минимум, пол часа времени! А если бы я допустил какие-то ошибки в коде и занялся бы их вылавливанием, что иногда случается, то еще больше.

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

Главное, что он смог, как умелый разработчик сделать реинжиниринг нужного мне куска алгоритма. Мне кажется эта штука изменит подход к разработке. Если раньше мы все делали своими руками, то теперь у меня в распоряжении идеальный раб=) Я продумываю логику и результат, затем говорю, что хочу получить, и вуаля - он дает мне это через секунду.

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

А еще мне нравится программисткий Low-Code подход на платформе Metabot. Он хорошо подходит для разработки в паре с ИИ. На других конструкторах ботов, особенно с визуальными редакторами, мне приходится извращаться, чтобы сформировать промпт, а в Metabot я просто добавляю к промпту содержимое экрана из Metabot со всеми сообщениями и кодом, и нейронка понимает мою задачу.

Выводы и рекомендации

Ключевое осознание, которое произошло в ходе применения ChatGPT в разработке кода, заключается в том, что ИИ вполне заменяет Junior-программиста. И даже его превосходит - ИИ знает больше и умеет больше чем юниор! И ошибается реже и учится быстрее. А главное, что он не устает и всегда доступен, мгновенно решая задачи. В то время как юниоры уходят на несколько часов, потом приносят какую-то кривую штуку с ошибками и все повторяется по кругу.

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

Итак, без сомнения, ИИ изменит подход к разработке, выступив в роли junior-программиста, эффективно справляющегося с рутинными задачами более опытного коллеги-разработчика/человека-оператора ИИ. Главная ценность GPT раскрывается, когда за его "рулем" находится опытный разработчик. Такой оператор ИИ хорошо осведомлен о своих целях и может четко и ясно передать эти задачи машине. ИИ забирает на себя выполнение стандартных операций, освобождая время разработчика для более глубокой, творческой и системной работы.

Практические рекомендации

  1. Понимание Роли GPT/ИИ:

    • Рассматривайте ИИ как инструмент в руках скульптора, где вы - скульптор, а ИИ - ваше средство для ваяния кода по вашему замыслу. 
    • Воспринимайте ИИ не как замену разработчика, а как junior-разработчика для работы в паре, готового мгновенно и беспрекословно выполнить любую задачу.
    • Это инструмент, который может исполнять задачи по вашему указанию, но не способен самостоятельно создавать или развивать сложные концепции.
    • Воспринимайте ИИ как дополнение к вашим навыкам, а не как полную замену. Он может забрать на себя выполнение рутинной работы, позволяя вам сосредоточиться на более творческих и стратегических аспектах проекта. 
    • Используйте ИИ для выполнения повторяющихся и времязатратных операций, чтобы сосредоточиться на более важных аспектах разработки. Например, реинжиниринг куска кода, добавление комментариев, написание документации.
  2. Комбинирование Low-Code и ИИ:

    • Использование Low-Code вместе с ИИ позволяет многократно повышать продуктивность, снижая время, необходимое для разработки и реализации сложных функций.
  3. Четкое Задание Задач:

    • Удостоверьтесь, что ваши инструкции для ИИ четкие и точные. Это уменьшит количество ошибок и повысит качество выполнения задач.
  4. Необходимость Навыков Программирования:

    • Для эффективного использования ИИ необходимы базовые знания в области программирования, создания архитектуры и системного мышления. Это позволит вам точно формулировать задачи и корректировать результаты работы ИИ.
  5. Принятие Активной Роли в Процессе:

    • Помните, что ИИ не заменяет разработчика, а действует как его помощник. Будьте готовы вмешаться и вносить коррективы, когда это необходимо.
    • Вы должны активно участвовать в процессе работы с ИИ, особенно когда речь идет о сложных и нестандартных задачах. 
  6. Использование Средств Сравнения Кода:

    • Применяйте программы типа WinMerge для сравнения сгенерированного GPT кода с текущим. Это позволит вам быстро выявлять различия и упростит процесс дебаггинга.
  7. Тестирование и Проверка Кода:
    • Тщательно тестируйте сгенерированный код. Всегда проверяйте его на предмет ошибок и совместимости с вашими текущими системами.
    • Всегда тщательно тестируйте код, полученный от ИИ, прежде чем использовать его в реальных проектах. Проверяйте на ошибки и убедитесь, что он соответствует вашим требованиям.
  8. Обработка Ошибок:

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

Заключение

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

Это возможность значительно ускорить разработку, передавая ИИ рутинные задачи и фокусируясь на более сложных и творческих аспектах проекта.

ИИ становится незаменимым инструментом в арсенале современного разработчика, позволяя ему максимально раскрыть свой творческий и интеллектуальный потенциал.

Удачи в освоении Low-Code в паре с ИИ!