7 min read

Вебхук (Webhook)

Вебхук (Webhook)
Фото: Dawid Zawiła / Unsplash

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

Пример. Посмотрим, как работает вебхук, связывающий DialogFlow и базу данных MySQL на языке PHP. Нам предстоит развернуть некоторое ПО на своем компьютере.

Настройка локального сервера

Для начала установим свежую версию локального сервера XAMPP. Это ПО позволит сделать наш скрипт исполняемым при обращении к последнему в адресной строке браузера и доступным веб-консоли DialogFlow в целом. Сервер можно скачать на сайте apachefriend.org. Если вы работаете на Windows, используйте стандартный установочный путь 'C://xampp'. После установки утилита выглядит следующим образом:

Если вы используете Windows, запустите Apache и MySQL кнопками Start:

Запустим локальный сервер кнопкой Start, затем во вкладке Services запустим MySQL кнопкой Start All:

Во вкладке Network выделим localhost:8080 и запустим его кнопкой Enable:

Во вкладке Volumes нажмем Mount, а затем Explore. Откроется проводник с папкой сетевого тома, который доступен только при запущенном сервере XAMPP. В эту директорию мы и отправим наш скрипт, связывающий MySQL и DialogFlow

В открывшейся директории найдем папку htdocs и сделаем ее избранной папкой в нашем проводнике. Затем создадим внутри нее папку 'RestaurantReservation' и откроем в редакторе кода. Я буду использовать VSCode.

В этом разделе дело за малым – дать возможность DialogFlow подступиться к нашему локальному серверу. С этим поможет утилита ngrok, скачаем ее по ссылке. Распаковываем архив и перемещаем исполняемый одноименный файл в нашу директорию RestaurantReservation.

macOS: откроем терминал в VSCode и запустим ngrok командой ngrok http 8080.

Windows: откроем приложение и запустим переадресацию с помощью ngrok http 8080. Стоит использовать именно этот стандартный для XAMPP порт 8080, чтобы DF смог добраться до нашего скрипта.

Мы получили https-ссылку https://dd24-188-234-79-4.ngrok.io, ее вскоре и будем использовать:

Настройка базы данных

Чтобы связка с БД работала, нам предстоит наладить стандартное соединение с MySQL, которую мы в XAMPP уже запустили. Для этого создадим файл db.php и зальем в него стандартные настройки  (они могут слегка разниться в зависимости от версии программы) – тип сервера, логин и пароль для подключения, база по умолчанию и хост (localhost – это зарезервированное вашим ПК доменное имя). С помощью встроенной функции mysqli_connect() мы и производим подключение, используя переменные выше как аргументы. Если соединение по какой-то причине установить не удалось, функция die() отобразит сообщение с причиной ошибки:

$mySQLserver = "localhost";
$mySQLuser = "root";
$mySQLpassword = "";
$mySQLdefaultdb = "DialogFlowRestaurantReservation";
$host = "localhost";

$link = mysqli_connect($mySQLserver, $mySQLuser, $mySQLpassword,$mySQLdefaultdb) or die (mysql_error());

Теперь перейдем в браузерную админку MySQL нажатием кнопки Admin в XAMPP или переходом по адресу http://localhost:8080/phpmyadmin/index.php (или http://localhost/phpmyadmin/) и создадим базу данных. Нажав кнопку "Создать БД", мы попадем в окно настроек, где задать стоит только имя базы данных DialogFlowRestaurantReservation. Жмем "Создать".

Переходим во вкладку SQL, вставляем код запроса (полный код здесь), создающего таблицу users с бронированиями и несколько примеров и жмем "Вперед":

Теперь при нажатии на название базы в меню слева мы сможем перейти к полученной таблице, в которую добавили три примера:

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

Создание скрипта

Создадим в папке RestaurantReservation файл MakeReservation.php:

Для начала включим в скрипт db.php, производящий соединение с MySQL:

include("db.php");

Функция sendMessage() отправит сообщение от имени бота:

function sendMessage($parameters) {
echo json_encode($parameters);
}

Мы будем сохранять текст пользовательского ответа во временный файл add.txt, и чтобы вычленить из него данные полей в корректном формате, создадим функцию debug_text():

function debug_text($filename, $contentdebug) {
$myfile = fopen($filename, "w") or die("Не удается открыть файл!");
fwrite($myfile, $contentdebug);
fclose($myfile);
}

Следующий игрок – функция add_data(), непосредственно добавляющая в БД новую запись, оставленную в файле:

function add_data($stand_number, $first_name, $last_name,
$stand_size, $date_purchased, $phone_number, $mySQLserver, $mySQLdefaultdb, $mySQLuser, $mySQLpassword) {

$sql = "insert into users (stand_number, first_name, last_name,
stand_size, date_purchased, phone_number, date_purchased) values
(
'$stand_number', '$first_name', '$last_name',
'$stand_size', '$date_purchased', '$phone_number')";

$filename = "add.txt";
$contentdebug = "sql=".$sql;
debug_text($filename, $contentdebug);

$last_id=0;
try {
		$conn = new PDO("mysql:host = $mySQLserver; dbname = $mySQLdefaultdb", $mySQLuser, $mySQLpassword);
		$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
		$conn -> exec($sql);
		$last_id = $conn -> lastInsertId();
	}
catch(PDOException $e)
{
	$last_id = 0;
}

return $last_id;

}

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

date_default_timezone_set("Europe/Moscow");
$CurrentDay = date("Y/m/d");
$CurrentHour = date("H:i:sa");
$DateTime = $CurrentDay. " ".$CurrentHour;

Создадим команду для получения названия action-функции из диалогового массива, который перенаправляется на этот сервер:

$update_response = file_get_contents("php://input");
$update = json_decode($update_response, true);
$varresultaction = $update["queryResult"]["action"];

На самом деле общение бота и человека – это обмен немаленькими JSON-массивами, и умение читать их позволит вам настраивать какую угодно логику. Чтобы добраться до диагностической информации, после старта диалога в консоли нажмите кнопку Diagnostic Info:

Разверзнутся хляби небесные и мы получим полезнейшую информацию о происходящем у DialogFlow под капотом. Вкладка 'Raw API Response' покажет полное состояние диалога на момент последней реплики, с идентификатором сессии, Контекстами (Context), параметрами и проч. В разделе "Создание скрипта" мы уже научились вычленять из массива любые параметры. Выделяем мы их из вкладки 'Fulfillment Request':

Вкладка 'Fulfillment Status' даст понять, исполнился ли вебхук и укажет на причину ошибки в случае чего.

Научим скрипт выделять реплику пользователя из массива ответа:

$messages = $update["queryResult"]["queryText"];
$session = $update["session"];

Получим данные полей из ответного массива:

// Идентификатор записи
$id = (isset($update["queryResult"]["parameters"]["id"]) ? $update["queryResult"]["parameters"]["id"] : null);

// номер забронированного столика
$standnumber = (isset($update["queryResult"]["parameters"]["standnumber"]) ? $update["queryResult"]["parameters"]["standnumber"] : null);

// размер забронированного столика
$standsize = (isset($update["queryResult"]["parameters"]["standsize"]) ? $update["queryResult"]["parameters"]["standsize"] : null);

// имя, фамилию и телефон бронирующего
$firstname = (isset($update["queryResult"]["parameters"]["firstname"]) ? $update["queryResult"]["parameters"]["firstname"] : null);

$lastname = (isset($update["queryResult"]["parameters"]["lastname"]) ? $update["queryResult"]["parameters"]["lastname"] : null);

$phonenumber = (isset($update["queryResult"]["parameters"]["phonenumber"]) ? $update["queryResult"]["parameters"]["phonenumber"] : null);

// Сгенерируем дату бронирования
$datepurchased = (isset($update["queryResult"]["parameters"]["datepurchased"]) ? $update["queryResult"]["parameters"]["datepurchased"] : null);
// Уберем избыточную часть временного штампа
$datepurchased = str_replace("T12:00:00+07:00", "", $datepurchased);

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

$ignore1 = "";
$sessionunique = "";

if ($session != "") {
$list = list($ignore1, $sessionunique) = explode('sessions/', $session);
}
else {
$nothing = "Нет активной сессии";
sendMessage(array(
"source" => null,
"fulfillmentText" => $nothing
));
exit;
}

В "эпицентре" скрипта – функция entry_data, отыскивающая последний ID в таблице MySQL, генерирующей новый, увеличены на единицу, и создающая запись на основе ответа юзера:

if ($varresultaction == "entry_data") {
// Узнаем последний использованный ID в БД и назначим новой записи .// идентификатор, увеличенный на единицу
$last_id = add_data($standnumber, $firstname, $lastname, $standsize, $datepurchased, $phonenumber, $DateTime, $link, $mySQLserver, $mySQLdefaultdb, $mySQLuser, $mySQLpassword);
$response = "Запись создана, номер заказа - ".$last_id;
sendMessage(array(
"source" => null,
"fulfillmentText" => $response
));
exit;
}

Настройка DialogFlow

Чтобы создать бота, перейдите по адресу dialogflow.cloud.google.com и в раскрывающемся списке слева нажмите 'Create new agent':

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

  • Agent name: RestaurantReservation (можно любое)
  • Default Language: Russian - ru
  • Default Time Zone: (GMT +3.00) Europe / Moscow

Нажмите Create. Теперь перейдите во вкладку Export and Import в настройках (кнопка-шестеренка). Восстановите бота из архива, который можно скачать по ссылке.

Теперь перейдем в раздел Fulfillment, разрешим Webhook и поместим https-ссылку с дополнением в виде названия папки и скрипта внутри (https://dd24-188-234-79-4.ngrok.io/RestaurantReservation/MakeReservation.php), сгенерированную ngrok в поле URL. Сохраним настройку нажатием Save.

При импорте мы получили небольшой набор намерений: приветствие, инициация (Начать бронирование), создание записи в MySQL (Создать бронирование) и ошибка (Фолбэк). Чтобы начать диалог, наберем в консоли справа "Создать запись". Система перейдет в намерение "Начать бронирование" и предложит нам шаблон ответа:

Прежде чем отвечать на вопрос: заглянем в намерение "Создать бронирование" и обратим внимание на пару нюансов. В разделе Action and parameters можно заметить action-функцию entry_data,  которую мы ранее создавали в нашем скрипте. Она-то и будет собирать из реплики пользователя поля.

Главное – разрешить исполнение вебхуков в секции внизу:

Копируем шаблон и заполним поля: "Номер столика: 4 Имя: Helen Фамилия: Kapatsa Число гостей: 3 Дата бронирования: 2022-01-23 Телефон: 89627276031".  Готово, запись создана и доступна в админке:

Все файлы урока можно найти в этой облачной папке.