Материал, перевод которого мы сегодня представляем вашему вниманию, посвящён разработке чат-бота для Facebook Messenger. Бот, который называется Aww Bot, общаясь с пользователями, будет отправлять им картинки симпатичных котов и собак.



Начало работы


Начнём с создания страницы на Facebook, заполнив необходимые поля. Эта страница предназначена для бота. Кроме того, создадим Facebook-приложение, после чего, на странице Add a Product, подключим к приложению продукт Messenger. Далее, мы окажемся на странице настройки мессенджера. Здесь надо найти раздел Token Generation, в нём — выбрать страницу бота в списке Page. После этого нас спросят о разрешениях и будет создан токен доступа. Бот будет использовать этот токен для выполнения вызовов к API Facebook Messenger, что позволит ему общаться с пользователями.

Настройка веб-сервера


Мы, для создания HTTP-сервера, будем использовать node.js и express.js. Выполним следующую команду:

npm install express body-parser request config --save

Добавим в index.js следующий код, который позволит создать простой HTTP-сервер:

'use strict';
let express = require('express'),
    bodyParser = require('body-parser'),
    app = express();
 
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
 
app.listen(8989, () => console.log('Example app listening on port 8989!'));
 
app.get('/', (req, res) => res.send('Hello World!'));

Теперь, если запустить сервер и перейти, с помощь браузера, по адресу http://127.0.0.1:8989, можно будет увидеть страницу с ответом сервера — Hello World!.

HTTPS и локальное окружение разработки


Прежде чем переходить к работе с технологией Webhook, нам нужно настроить HTTPS для окружения разработки. Мессенджер не примет адрес Webhook, используемый для отправки уведомлений нашему серверу, если вы используете самоподписанный SSL-сертификат. Бесплатный сертификат можно получить от Let’s Encrypt. Тут, правда, можно получить сертификат только на домен, а не на IP-адрес. Мы воспользуемся сервисом ngrok, который позволит организовать доступ к локальному серверу через общедоступный URL, который использует HTTPS.

Настройка ngrok


Настроить ngrok несложно. Нужно лишь загрузить сжатый архив с сайта проекта, распаковать его и выполнить следующую команду:

./ngrok http 80

Не забудьте перенаправить порт 80 на 8989 в WAN-настройках вашего маршрутизатора. В результате ngrok создаст общедоступные HTTP и HTTPS-адреса для локального сервера.

Работа с Webhook-уведомлениями


Мессенджер использует технологию Webhook для аутентификации и для передачи уведомлений о происходящих событиях вашему приложению. С точки зрения программирования всё это сводится к работе обычных функций обратного вызова для обработки HTTP-запросов, которые будут получать данные о событиях, вроде полученных чат-ботом сообщений. Для разбора GET и POST-запросов мы будем использовать модуль body-parser.

Добавим в приложение следующий маршрут. Он нужен для обработки Webhook-запросов на верификацию.

// Добавление поддержки GET-запросов в webhook
app.get('/webhook', (req, res) => {
 
    // Токен верификации. Он должен быть строкой, состоящей из случайных символов
    let VERIFY_TOKEN = "SOMETHING_RANDOM";
 
    // Разбор параметров запроса
    let mode = req.query['hub.mode'];
    let token = req.query['hub.verify_token'];
    let challenge = req.query['hub.challenge'];
 
    // Проверка, имеются ли в запросе mode и token 
    if (mode && token) {
 
        // Проверка правильности mode и token
        if (mode === 'subscribe' && token === VERIFY_TOKEN) {
 
            // Отправка токена challenge из запроса
            console.log('WEBHOOK_VERIFIED');
            res.status(200).send(challenge);
 
        } else {
            // Отправка ответа '403 Forbidden' если верифицировать токен не удалось
            res.sendStatus(403);
        }
    }
});

Теперь надо открыть настройки мессенджера, найти там раздел Webhooks и настроить интеграцию приложения с Webhook-уведомлениями. На странице настроек, в поле Callback URL, надо ввести наш HTTPS URL, полученный от ngrok. Токен верификации (тот, который присутствует в коде и представляет собой созданную нами случайную строку) надо поместить в поле Verify Token. После этого у вас должно получиться верифицировать и сохранить настройки, нажав на кнопку Verify and Save, если ваш URL для обработки Webhook-уведомлений доступен, и токен верификации соответствует тому, который имеется в коде.


Настройка токена и URL для получения приложением Webhook-уведомлений

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

Теперь создайте POST-маршрут для обработки POST-событий от мессенджера. Добавьте в приложение следующий код.

// Создание конечной точки для webhook
app.post('/webhook', (req, res) => {
 
    let body = req.body;
 
    if (body.object === 'page') {
 
        //Перебор объектов, которых может быть несколько при пакетной передаче данных
        body.entry.forEach(function(entry) {
 
            // сущность entry.messaging является массивом, но
            // тут будет лишь одно сообщение, поэтому используется индекс 0
            let webhook_event = entry.messaging[0];
            console.log(webhook_event);
 
            // Получение PSID отправителя
            let sender_psid = webhook_event.sender.id;
            console.log('Sender PSID: ' + sender_psid);
 
            // Проверка события, выяснение того, message это или postback,
            // и передача события подходящей функции-обработчику
            if (webhook_event.message) {
                console.log(webhook_event.message)
            } else if (webhook_event.postback) {
                console.log(webhook_event.postback)
            }
        });
 
        // Возврат '200 OK' в ответ на все запросы
        res.status(200).send('EVENT_RECEIVED');
    } else {
        // Возврат '404 Not Found', если событие не относится к тем, на которые мы подписаны
        res.sendStatus(404);
    }
 
});

Мы настроили приложение таким образом, чтобы оно обрабатывало два типа событий — message и postback. Для того чтобы проверить работу механизма Webhook-уведомлений, откройте мессенджер и отправьте странице бота сообщение. Если всё работает как надо, в лог попадут PSID отправителя, сведения о событии и содержимое сообщения. Теперь напишем функции-обработчики для интересующих нас событий.

// Обработка события message
const handleMessage = (sender_psid, received_message) => {
    let response;
 
    if (received_message.text) {
 
    }
}
 
// Обработка события postback
const handlePostback = (sender_psid, received_postback) => {
    let response;
 
    // Получение данных события postback
    let payload = received_postback.payload;
 
    if(payload === 'GET_STARTED'){
 
    }
}

Метод handleMessage() отвечает за обработку входящих сообщений, а метод handlePostback() — за обработку входящих событий postback. Обновите существующий код, добавив туда вызовы этих методов:

// Проверка события
// и передача события подходящей функции-обработчику
if (webhook_event.message) {
    handleMessage(sender_psid, webhook_event.message);
} else if (webhook_event.postback) {
    handlePostback(sender_psid, webhook_event.postback);
}

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

Настройка экрана приветствия и postback-события начала диалога с ботом


Когда новый пользователь начинает беседу с ботом, в окне чата выводится кнопка Get Started. Можно настроить собственное postback-событие для этой ситуации. Например, задать выдачу сообщения для пользователя, которое описывает бота и то, как с ним общаться. Для того чтобы настроить собственное приветствие, выполните эту команду curl в терминале:

curl -X POST -H "Content-Type: application/json" -d '{
  "greeting": [
    {
      "locale":"default",
      "text":"Hello {{user_first_name}}! Are you ready to see the cutests cats and dogs"
    }
  ]
}' "https://graph.facebook.com/v2.6/me/messenger_profile?access_token=YOUR_PAGE_ACCESS_TOKEN"

Мы настроили Aww Bot так, чтобы он выводил сообщение, спрашивая пользователя о том, готов ли он увидеть самых симпатичных кошек и собак. Для того чтобы настроить postback-событие, выполните в терминале эту команду:

curl -X POST -H "Content-Type: application/json" -d '{
  "get_started": {"payload": "GET_STARTED"}
}' "https://graph.facebook.com/v2.6/me/messenger_profile?access_token=YOUR_PAGE_ACCESS_TOKEN"

Вот как выглядит сеанс начала чата с ботом.


Экран начала работы

Настройка приложения


Мы будем использовать модуль конфигурации npm для хранения токена доступа к странице в отдельном конфигурационном файле. Создадим директорию config в нашем проекте и файл default.json в ней. В этот файл надо добавить токен доступа к странице и сделать запись об этом файле в .gitignore.

{
  "facebook": {
    "page": {
      "access_token": "PAGE_ACCESS_TOKEN"
    }
  }
}

Мы будем получать токен доступа к странице в методе callSendAPI(), пользуясь командой config.get('facebook.page.access_token').

Обработка события начала работы


Вот код обработки события начала работы.

const handlePostback = (sender_psid, received_postback) => {
    let response;
 
    // Получим данные postback-уведомления
    let payload = received_postback.payload;
 
    if(payload === 'GET_STARTED'){
        response = askTemplate('Are you a Cat or Dog Person?');
        callSendAPI(sender_psid, response);
    }
}

Создадим метод askTemplate(), который будет возвращать правильно подготовленный объект ответа для API мессенджера. Метод callSendAPI() будет отправлять сообщение пользователю. Добавьте в приложение следующие методы:

const askTemplate = (text) => {
    return {
        "attachment":{
            "type":"template",
            "payload":{
                "template_type":"button",
                "text": text,
                "buttons":[
                    {
                        "type":"postback",
                        "title":"Cats",
                        "payload":"CAT_PICS"
                    },
                    {
                        "type":"postback",
                        "title":"Dogs",
                        "payload":"DOG_PICS"
                    }
                ]
            }
        }
    }
}
 
// Отправка ответного сообщения через API Send
const callSendAPI = (sender_psid, response, cb = null) => {
    // Конструируем тело сообщения
    let request_body = {
        "recipient": {
            "id": sender_psid
        },
        "message": response
    };
 
    // Отправляем HTTP-запрос к Messenger Platform
    request({
        "uri": "https://graph.facebook.com/v2.6/me/messages",
        "qs": { "access_token": config.get('facebook.page.access_token') },
        "method": "POST",
        "json": request_body
    }, (err, res, body) => {
        if (!err) {
            if(cb){
                cb();
            }
        } else {
            console.error("Unable to send message:" + err);
        }
    });
}

Мы отправляем пользователю сообщение, содержащее две кнопки и текст. Когда пользователь выберет то, что ему нужно, щёлкнув по соответствующей кнопке, на наш Webhook-адрес будет отправлен запрос с данными события postback и мы обработаем его.


Пользователю предлагается выбрать интересующий его вид изображений

Обработка собственных событий postback


Обновим код функции-обработчика события postback:

const handlePostback = (sender_psid, received_postback) => {
    let response;
 
    // Получим данные postback-уведомления
    let payload = received_postback.payload;
 
    // Сформируем ответ, основанный на данных уведомления
    if (payload === 'CAT_PICS') {
        response = imageTemplate('cats', sender_psid);
        callSendAPI(sender_psid, response, function(){
            callSendAPI(sender_psid, askTemplate('Show me more'));
        });
    } else if (payload === 'DOG_PICS') {
        response = imageTemplate('dogs', sender_psid);
        callSendAPI(sender_psid, response, function(){
            callSendAPI(sender_psid, askTemplate('Show me more'));
        });
    } else if(payload === 'GET_STARTED'){
        response = askTemplate('Are you a Cat or Dog Person?');
        callSendAPI(sender_psid, response);
    }
    // Отправим сообщение
}

Когда пользователь щёлкает по кнопке Cats, на наш адрес, используемый для обработки Webhook-уведомлений, поступит запрос с событием postback, содержащим данные CAT_PICS. Выбор варианта Dogs приведёт к отправке события postback с данными DOG_PICS. Мы добавили в систему ещё один метод, imageTemplate(), который возвращает сообщение, содержащее ссылку на изображение кошки или собаки.

Создание простого API, возвращающего ссылки на изображения


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

module.exports = {
    cats : [
        'https://i.imgur.com/Qbg7CeM.jpg',
        'https://i.imgur.com/nUzkpJY.jpg',
        'https://i.imgur.com/NpDcKph.jpg',
        'https://i.imgur.com/oJtSDaO.jpg',
        'https://i.redd.it/82ajpsrd17111.jpg',
        'https://i.redd.it/00km1d2rt0111.jpg',
        'https://i.redd.it/rdbavhp0y7111.jpg',
        'https://i.redd.it/5hn3mg0n98111.jpg',
        'https://i.redd.it/d23pb8mta6111.jpg',
        'https://i.redd.it/d2gyrwgy7oz01.jpg',
        'https://i.redd.it/z4sgl84q72z01.jpg',
        'https://i.redd.it/wvykzo8n1cy01.jpg'
    ],
 
    dogs : [
        'https://i.redd.it/6tjihi2qe7111.jpg',
        'https://i.imgur.com/etRCs56.jpg',
        'https://i.redd.it/nibw50f8y4111.jpg',
        'https://i.redd.it/izcvnvj1o7111.jpg',
        'https://i.redd.it/eqs1g9dldz011.jpg',
        'https://i.redd.it/civ9dnu9u1111.jpg',
        'https://i.redd.it/kk03qwclkp011.jpg',
        'https://i.redd.it/2694pupjne011.jpg',
        'https://i.redd.it/qk49ls5y6oy01.jpg',
        'https://i.imgur.com/oM3mKgB.jpg',
        'https://i.redd.it/8kx2riaulux01.jpg'
    ]
};

Теперь подключим его в приложении.

images = require('./pics');

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

const = imageTemplate(type, sender_id) => {
    return {
        "attachment":{
            "type":"image",
            "payload":{
                "url": getImage(type, sender_id),
                "is_reusable":true
            }
        }
    }
}

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

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

let users = {};
 
const = getImage(type, sender_id) => {
    // если записи о пользователе пока нет - создадим её
    if(users[sender_id] === undefined){
        users = Object.assign({
            [sender_id] : {
                'cats_count' : 0,
                'dogs_count' : 0
            }
        }, users);
    }
 
    let count = images[type].length, // общее количество изображений нужного типа
        user = users[sender_id], // пользователь, ожидающий ответа
        user_type_count = user[type+'_count'];
 
 
    // обновим сведения о пользователе до отправки ответа
    let updated_user = {
        [sender_id] : Object.assign(user, {
            [type+'_count'] : count === user_type_count + 1 ? 0 : user_type_count + 1
        })
    };
    // обновим список пользователей
    users = Object.assign(users, updated_user);
 
    console.log(users);
    return images[type][user_type_count];
}

Мы храним PSID каждого из пользователей, общающихся с ботом, в виде ключа в объекте users. Если записи о пользователе пока нет, создаём новую запись. Будем обновлять сведения о номере изображения каждый раз, когда пользователь запрашивает картинку кошки или собаки. Затем возвращаем абсолютный путь к изображению, которое будет использоваться в шаблоне сообщения. Далее, отправляем сообщение с изображением в виде ответа на postback-событие, генерируемого тогда, когда пользователь выбирает тип интересующего его изображения.

// Настроим ответ, основываясь на данных postback-уведомления
if (payload === 'CAT_PICS') {
    response = imageTemplate('cats', sender_psid);
    callSendAPI(sender_psid, response, function(){
        callSendAPI(sender_psid, askTemplate('Show me more'));
    });
} else if (payload === 'DOG_PICS') {
    response = imageTemplate('dogs', sender_psid);
    callSendAPI(sender_psid, response, function(){
        callSendAPI(sender_psid, askTemplate('Show me more'));
    });
} else if(payload === 'GET_STARTED'){
    response = askTemplate('Are you a Cat or Dog Person?');
    callSendAPI(sender_psid, response);
}

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


Общение с ботом

Итоги


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

Уважаемые читатели! Планируете ли вы создавать ботов для Facebook Messenger?

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


  1. Tantrido
    19.06.2018 15:50

    Работа с Webhook-уведомлениями
    Очень сложно. Поставь botbuilder и разработка такого бота упростится в разы: habr.com/post/333824 — не нужно будет изобретать велосипеды! Этот бот разрабатывался под Facebook: habr.com/post/340092


    1. ZOXEXIVO
      19.06.2018 20:36

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


      1. Tantrido
        19.06.2018 21:26

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

        постоянно сломанная обратная совместимость
        Не сталкивался.

        и проксирование трафика через сервера MS, которые иногда дают ощутимый лаг в запросах.
        У нас приложение работало на Azure — всё работало реактивно.