В настоящее время наблюдается, действительно, бум чат-мессенджеров. Один за другим платформы для обмена мгновенными сообщениями объявляют о запуске платформы для разработки ботов.
Не стал и исключением Facebook. 12 апреля на конференции F8 Facebook представила платформу для разработки ботов для своего мессенджера.
В данной статье хочу поделиться опытом разработки чат-бота для Facebook на PHP.

Общая информация


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

Создание страницы


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

Регистрация и настройка приложения


Переходим к регистрации своего приложения в аккаунте разработчика.
Заходим по ссылке developers.facebook.com/apps
Нажимаем на добавление нового приложения, выбираем другой настройку вручную:



Далее заполняем форму:



После создания приложения, в левом меню выбираем вкладку Messenger и кликаем на нее.
Нажимаем «Начать».
В первую очередь выбираем страницу, созданную для бота, и копируем token. Сохраняем его где-нибудь, он нам пригодится дальше.



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

<?php
$verify_token = ""; // Verify token 
if (!empty($_REQUEST['hub_mode']) && $_REQUEST['hub_mode'] == 'subscribe' && $_REQUEST['hub_verify_token'] == $verify_token) { 
echo $_REQUEST['hub_challenge']; 
}

В переменную $verify_token необходимо добавить какой-то текст.
Скрипт загружаем на сервер. Допустим, наш скрипт доступен по адресу: domain.com/fbbot

Возвращаемся ко вкладке Messenger в настройках приложения FB.
Ищем блок «Webhooks» и кнопку «Setup Webhooks». Кликаем на нее.

В поле «Обратный URL-адрес» указываем адрес нашего бота — domain.com/fbbot
SSL — сертификат является обязательным. Самоподписанный сертификат не подойдет.

В поле «Подтвердить маркер» указываем тот текст, который указали в переменной $verify_token в скрипте.
В поле «Поля подписки» выбираем, какие уведомления мы хотим получать на наш webhook:
  • message_deliveries — уведомления о доставке сообщения
  • messages — сообщения, написанные пользователем боту
  • messaging_optins — callback при получении сообщения через кнопку на сайте (Send-to-Messenger Plugin)
  • messaging_postbacks — переходы по кнопкам из предыдущих сообщений бота (будет понятно далее)

Выбираем нужные и нажимаем кнопку «Подтвердить и сохранить».

Связываем приложение и страницу


Набираем в консоли:

curl -ik -X POST "https://graph.facebook.com/v2.6/me/subscribed_apps?access_token=-token-"

-token- заменяем на токен вашей страницы.

Типы сообщений в FB Messenger


Сообщения могут быть либо просто текстовые, либо Structured Text, которые в свою очередь могут быть:
  • button — кнопки
  • generic — элементы
  • receipt — счет на оплату

Кнопки (button)


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


Кнопки могут быть двух типов:
  1. Отправляющие ответ боту
  2. Переходящие по адресу в интернете

Важный момент: в одном таком сообщении может быть максимум 3 кнопки, при попытке отправить сообщение с бОльшим количеством кнопок — оно просто не дойдет до получателя.

Элементы (generic)


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


В одном сообщении может содержаться до 10 элементов. При наличии более одного элемента, появляется горизонтальная прокрутка.
Важный момент: в одном таком сообщении может быть максимум 3 кнопки, при попытке отправить сообщение с бОльшим количеством кнопок — оно просто не дойдет до получателя.

Счет на оплату (receipt)


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


Важный момент: номер счета д��лжен быть уникальным.

Пишем код


На момент написания бота, на GitHub еще не было реализации API на PHP, поэтому пришлось писать PHP SDK самостоятельно.

Устанавливаем PHP SDK для работы с FB Messenger API при помощи composer:
composer require "pimax/fb-messenger-php" "dev-master"

Создаем файл index.php:
<?php

$verify_token = ""; // Verify token
$token = ""; // Page token

if (file_exists(__DIR__.'/config.php')) {
    $config = include __DIR__.'/config.php';
    $verify_token = $config['verify_token'];
    $token = $config['token'];
}

require_once(dirname(__FILE__) . '/vendor/autoload.php');

use pimax\FbBotApp;
use pimax\Messages\Message;
use pimax\Messages\MessageButton;
use pimax\Messages\StructuredMessage;
use pimax\Messages\MessageElement;
use pimax\Messages\MessageReceiptElement;
use pimax\Messages\Address;
use pimax\Messages\Summary;
use pimax\Messages\Adjustment;

$bot = new FbBotApp($token);

if (!empty($_REQUEST['hub_mode']) && $_REQUEST['hub_mode'] == 'subscribe' && $_REQUEST['hub_verify_token'] == $verify_token)
{
     // Webhook setup request
    echo $_REQUEST['hub_challenge'];
} else {

     $data = json_decode(file_get_contents("php://input"), true);
     if (!empty($data['entry'][0]['messaging']))
     {
            foreach ($data['entry'][0]['messaging'] as $message)
            {
// Получено сообщение
// Основной код будет в этом блоке
// ...
            }
   }
}

Пробуем отправить сообщение пользователю в ответ при получении любого сообщения от него.
Для этого в блок получения сообщения, добавляем:
$bot->send(new Message($message['sender']['id'], ‘Hi there!'));


Проверяем. Находим в мессенджере нашего бота и пробуем отправить ему любое сообщение.
В ответ мы должны получить от него «Hi there!».
Важно: Пока приложение не прошло модерацию бот будет работать только для автора приложения.

Если все работает как надо, идем дальше.

В блок получения сообщения добавляем:
// Пропускаем обработку отметок о доставке сообщения
if (!empty($message['delivery'])) {
    continue;
}

$command = "";
// Получено сообщение от пользователя, записываем как команду
if (!empty($message['message'])) {
    $command = $message['message']['text'];
    // ИЛИ Зафиксирован переход по кнопке, записываем как команду
} else if (!empty($message['postback'])) {
    $command = $message['postback']['payload'];
}

// Обрабатываем команду
switch ($command) {

    // When bot receive "text"
    case 'text':
        $bot->send(new Message($message['sender']['id'], 'This is a simple text message.'));
        break;

    // When bot receive "button"
    case 'button':
      $bot->send(new StructuredMessage($message['sender']['id'],
          StructuredMessage::TYPE_BUTTON,
          [
              'text' => 'Choose category',
              'buttons' => [
                  new MessageButton(MessageButton::TYPE_POSTBACK, 'First button'),
                  new MessageButton(MessageButton::TYPE_POSTBACK, 'Second button'),
                  new MessageButton(MessageButton::TYPE_POSTBACK, 'Third button')
              ]
          ]
      ));
    break;

    // When bot receive "generic"
    case 'generic':

        $bot->send(new StructuredMessage($message['sender']['id'],
            StructuredMessage::TYPE_GENERIC,
            [
                'elements' => [
                    new MessageElement("First item", "Item description", "", [
                        new MessageButton(MessageButton::TYPE_POSTBACK, 'First button'),
                        new MessageButton(MessageButton::TYPE_WEB, 'Web link', 'http://facebook.com')
                    ]),

                    new MessageElement("Second item", "Item description", "", [
                        new MessageButton(MessageButton::TYPE_POSTBACK, 'First button'),
                        new MessageButton(MessageButton::TYPE_POSTBACK, 'Second button')
                    ]),

                    new MessageElement("Third item", "Item description", "", [
                        new MessageButton(MessageButton::TYPE_POSTBACK, 'First button'),
                        new MessageButton(MessageButton::TYPE_POSTBACK, 'Second button')
                    ])
                ]
            ]
        ));

    break;

    // When bot receive "receipt"
    case 'receipt':

        $bot->send(new StructuredMessage($message['sender']['id'],
            StructuredMessage::TYPE_RECEIPT,
            [
                'recipient_name' => 'Fox Brown',
                'order_number' => rand(10000, 99999),
                'currency' => 'USD',
                'payment_method' => 'VISA',
                'order_url' => 'http://facebook.com',
                'timestamp' => time(),
                'elements' => [
                    new MessageReceiptElement("First item", "Item description", "", 1, 300, "USD"),
                    new MessageReceiptElement("Second item", "Item description", "", 2, 200, "USD"),
                    new MessageReceiptElement("Third item", "Item description", "", 3, 1800, "USD"),
                ],
                'address' => new Address([
                    'country' => 'US',
                    'state' => 'CA',
                    'postal_code' => 94025,
                    'city' => 'Menlo Park',
                    'street_1' => '1 Hacker Way',
                    'street_2' => ''
                ]),
                'summary' => new Summary([
                    'subtotal' => 2300,
                    'shipping_cost' => 150,
                    'total_tax' => 50,
                    'total_cost' => 2500,
                ]),
                'adjustments' => [
                    new Adjustment([
                        'name' => 'New Customer Discount',
                        'amount' => 20
                    ]),

                    new Adjustment([
                        'name' => '$10 Off Coupon',
                        'amount' => 10
                    ])
                ]
            ]
        ));

    break;

    // Other message received
    default:
        $bot->send(new Message($message['sender']['id'], 'Sorry. I don’t understand you.'));
}

Пробуем отправить боту сообщения:
  • text
  • button
  • generic
  • receipt

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

Реальный пример Бота для фриланс-биржи Job4Joy


Итак, наша цель, реализовать бота, который по нашему запросу будет выдавать новые проекты в соответствующей категории.
Данные будем получать по RSS, используя picoFeed — github.com/fguillot/picoFeed

Выполняем:
composer require fguillot/picofeed @stable
composer require "pimax/fb-messenger-php" "dev-master"

Создаем файл index.php следующего содержания (комментарии приведены в коде):
<?php

$verify_token = ""; // Verify token
$token = ""; // Page token
$config = []; // config

if (file_exists(__DIR__.'/config.php')) {
    $config = include __DIR__.'/config.php';
    $verify_token = $config['verify_token'];
    $token = $config['token'];
}

require_once(dirname(__FILE__) . '/vendor/autoload.php');

use PicoFeed\Reader\Reader;
use pimax\FbBotApp;
use pimax\Messages\Message;
use pimax\Messages\MessageButton;
use pimax\Messages\StructuredMessage;
use pimax\Messages\MessageElement;

$bot = new FbBotApp($token);

if (!empty($_REQUEST['hub_mode']) && $_REQUEST['hub_mode'] == 'subscribe' && $_REQUEST['hub_verify_token'] == $verify_token)
{
    // Webhook setup request
    echo $_REQUEST['hub_challenge'];
} else {

    $data = json_decode(file_get_contents("php://input"), true);
    if (!empty($data['entry'][0]['messaging']))
    {
        foreach ($data['entry'][0]['messaging'] as $message)
        {
            if (!empty($data['entry'][0])) {

                if (!empty($data['entry'][0]['messaging']))
                {
                    foreach ($data['entry'][0]['messaging'] as $message)
                    {
                        if (!empty($message['delivery'])) {
                            continue;
                        }

                        $command = "";

                        if (!empty($message['message'])) {
                            $command = $message['message']['text'];
                        } else if (!empty($message['postback'])) {
                            $command = $message['postback']['payload'];
                        }

                        if (!empty($config['feeds'][$command]))
                        {
                            getFeed($config['feeds'][$command], $bot, $message);
                        } else {
                            sendHelpMessage($bot, $message);
                        }
                    }
                }
            }
        }
    }
}

/**
 * Send Help Message
 *
 * @param $bot Bot instance
 * @param array $message Received message
 * @return bool
 */
function sendHelpMessage($bot, $message)
{
    $bot->send(new StructuredMessage($message['sender']['id'],
        StructuredMessage::TYPE_BUTTON,
        [
            'text' => 'Choose category',
            'buttons' => [
                new MessageButton(MessageButton::TYPE_POSTBACK, 'All jobs'),
                new MessageButton(MessageButton::TYPE_POSTBACK, 'Web Development'),
                new MessageButton(MessageButton::TYPE_POSTBACK, 'Software Development & IT')
            ]
        ]
    ));

    $bot->send(new StructuredMessage($message['sender']['id'],
        StructuredMessage::TYPE_BUTTON,
        [
            'text' => ' ',
            'buttons' => [
                new MessageButton(MessageButton::TYPE_POSTBACK, 'Design & Multimedia'),
                new MessageButton(MessageButton::TYPE_POSTBACK, 'Mobile Application'),
                new MessageButton(MessageButton::TYPE_POSTBACK, 'Host & Server Management')
            ]
        ]
    ));


    $bot->send(new StructuredMessage($message['sender']['id'],
        StructuredMessage::TYPE_BUTTON,
        [
            'text' => ' ',
            'buttons' => [
                new MessageButton(MessageButton::TYPE_POSTBACK, 'Writing'),
                new MessageButton(MessageButton::TYPE_POSTBACK, 'Mobile Application'),
                new MessageButton(MessageButton::TYPE_POSTBACK, 'Marketing')
            ]
        ]
    ));

    $bot->send(new StructuredMessage($message['sender']['id'],
        StructuredMessage::TYPE_BUTTON,
        [
            'text' => ' ',
            'buttons' => [
                new MessageButton(MessageButton::TYPE_POSTBACK, 'Business Services'),
                new MessageButton(MessageButton::TYPE_POSTBACK, 'Translation & Languages')
            ]
        ]
    ));


    return true;
}

/**
 * Get Feed Data
 *
 * @param $url Feed url
 * @param $bot Bot instance
 * @param $message Received message
 * @return bool
 */
function getFeed($url, $bot, $message)
{
    try {
        $reader = new Reader;
        $resource = $reader->download($url);

        $parser = $reader->getParser(
            $resource->getUrl(),
            $resource->getContent(),
            $resource->getEncoding()
        );

        $feed = $parser->execute();
        $items = array_reverse($feed->getItems());

        if (count($items)) {
            foreach ($items as $itm)
            {
                $url = $itm->getUrl();
                $message_text = substr(strip_tags($itm->getContent()), 0, 80);

                $bot->send(new StructuredMessage($message['sender']['id'],
                    StructuredMessage::TYPE_GENERIC,
                    [
                        'elements' => [
                            new MessageElement($itm->getTitle(), $message_text, '', [
                                new MessageButton(MessageButton::TYPE_WEB, 'Read more', $url)
                            ]),

                        ]
                    ]
                ));
            }

        } else {
            $bot->send(new Message($message['sender']['id'], 'Not found a new projects in this section.'));
        }
    }
    catch (Exception $e) {
        writeToLog($e->getMessage(), 'Exception');
    }

    return true;
}

/**
 * Log
 *
 * @param mixed $data Data
 * @param string $title Title
 * @return bool
 */
function writeToLog($data, $title = '')
{
    $log = "\n------------------------\n";
    $log .= date("Y.m.d G:i:s") . "\n";
    $log .= (strlen($title) > 0 ? $title : 'DEBUG') . "\n";
    $log .= print_r($data, 1);
    $log .= "\n------------------------\n";

    file_put_contents(__DIR__ . '/imbot.log', $log, FILE_APPEND);

    return true;
}


И файл config.php следующего содержания:
<?php

return [
    'token' => '',   // Токен страницы
    'verify_token' => '',  // Проверочный токен
    'feeds' => [
        'All jobs' => 'https://job4joy.com/marketplace/rss/',
        'Web Development' => 'https://job4joy.com/marketplace/rss/?id=3',
        'Software Development & IT' => 'https://job4joy.com/marketplace/rss/?id=5',
        'Design & Multimedia' => 'https://job4joy.com/marketplace/rss/?id=2',
        'Mobile Application' => 'https://job4joy.com/marketplace/rss/?id=7',
        'Host & Server Management' => 'https://job4joy.com/marketplace/rss/?id=6',
        'Writing' => 'https://job4joy.com/marketplace/rss/?id=8',
        'Customer Service' => 'https://job4joy.com/marketplace/rss/?id=10',
        'Marketing' => 'https://job4joy.com/marketplace/rss/?id=11',
        'Business Services' => 'https://job4joy.com/marketplace/rss/?id=12',
        'Translation & Languages' => 'https://job4joy.com/marketplace/rss/?id=14',
    ]
];


Публикация в каталоге для всех


Пока бот доступен только для владельца аккаунта. Чтобы бот был доступен для всех, нужно На странице App Review — опубликовать приложение:



После этого нужно запросить модерацию мессенджера. Для этого переходим на вкладку — Messenger.
В блоке «App Review for Messenger» нажимаем кнопку «Request Permissions».
В появившемся окне выбираем «pages_messaging» и нажимаем «Add items».

Теперь остается только дождаться модерации.

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

Заключение


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

Полезные ссылки


  1. Getting Started with FB Chatbots — developers.facebook.com/docs/messenger-platform/quickstart
  2. Web hook Reference — developers.facebook.com/docs/messenger-platform/webhook-reference
  3. FB Messenger PHP API — github.com/pimax/fb-messenger-php
  4. Примеры использования PHP API — github.com/pimax/fb-messenger-php-example
  5. Job4Joy FB Bot — github.com/pimax/job4joy_fb