В настоящее время наблюдается, действительно, бум чат-мессенджеров. Один за другим платформы для обмена мгновенными сообщениями объявляют о запуске платформы для разработки ботов.
Не стал и исключением 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

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


  1. grin
    17.04.2016 20:12

    Счет на оплату (receipt) работает? Тоже разбираюсь сейчас с платформой, но пока до этого не дошел.


    1. pimax
      17.04.2016 22:47

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


  1. Diden05
    17.04.2016 23:42

    А есть какая то возможность получить данные отправителя, имя, емейл, телефон?


    1. pimax
      18.04.2016 16:51

      В $message['sender']['id'] из примеров выше мы имеем id пользователя, который отправил сообщение боту.

      Телефон не получим, а вот имя, фамилию и аватар — можем.
      Добавил в SDK метод userProfile.

      Теперь получить данные пользователя можно так:

      $bot->userProfile($message['sender']['id']);
      


      В ответ вернется объект UserProfile


      1. Diden05
        19.04.2016 02:15

        Отлично, спасибо.


  1. xilix
    18.04.2016 00:57

    Хочу сделать плагин для интернет-магазинов, но смущает требование сертификата. А что если поднять промежуточный сервер с сертификатом и api, и через него гонять всех клиентов? Как в таком случае быть с приложением? Оно тоже будет одно единственное для всех или можно как-то извратиться и сделать чтобы у каждого было свое?


    1. vosi
      18.04.2016 03:32

      Не парьтесь
      https://letsencrypt.org/


      1. akaluth
        18.04.2016 04:51

        CloudFlare тоже может прокатить. Telegram нормально дружит с их сертификатами, насчет фб не знаю.


    1. TimsTims
      18.04.2016 10:39

      Интернет-магазин и не может позволить себе установить бесплатный сертификат? Меня это смущает)))


  1. PavelBelyaev
    18.04.2016 04:51

    Спасибо, в закладки закину, на досуге попробую прикрутить, у меня в общем есть айпад с интернетом, который не умеет принимать СМС, у мегафона есть сервис UMS, но там блокируются СМС от банков, я клиент 6 инет-банков и мне потребовалось в них заходить даже тогда, когда моя мобилка разрядилась да и вообще не зависеть от телефона. Взял малинку, воткнул в нее модем ZTE, который умеет отвечать на CGI команды JSON ответами, дальше написал типа транспорт на VK, который мне СМС пересылает, но в один прекрасный момент вконтакт падал на полдня и я не мог зайти в банк, пришлось еще разобраться с tg (консольный клиент на telegramm), сейчас еще дублирует в телеграм, а у него есть веб интерфейс. Сама малинка не всегда тоже доступна по белому IP, т.к. при отпадании инета она переключается на мобильный, поэтому делать для нее веб-интерфейс не особо имело смысл.


    1. akaluth
      18.04.2016 04:56

      У телеграма сейчас есть замечательное Bot API, не требующее tg и отдельного номера.


      1. PavelBelyaev
        18.04.2016 05:00

        Ага, только я уже написал все скрипты, а потом прочитал про ботов, в TG сразу на LUA скриптики написал и с базой связку сделал, лень уже было что то перепиливать, да и времени нет лишнего, всё работает и отточено в течении длительного времени.


  1. beerhoff
    18.04.2016 10:25

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


    1. pimax
      18.04.2016 16:40

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

      Если речь про логирование на стороне FB — все сообщения привязываются к странице, администратор может просматривать все диалоги с ботом.


  1. artishok
    18.04.2016 14:29

    По-моему, пока только телеграм смог запустить нормальную бот-платформу


    1. horses
      18.04.2016 15:45

      Ну так телеграмм ее запустил уже давно. А фейсбук на прошлой неделе. Как бы пока сравнивать их не корректно :)


      1. artishok
        18.04.2016 15:49

        Да даже после запуска у телеграма все было просто и понятно. И главное — быстро.
        Создал бота, получил ключик, накидал логику(примитив) и добавил в чат — не более часа времени заняло.
        Здесь, как и в скайпе, только на модерацию может уйти несколько дней.


        1. pimax
          18.04.2016 16:37

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


          1. artishok
            19.04.2016 11:35

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


    1. kryvichh
      20.04.2016 18:59

      Поддержу. Telegram — единственная бот-платформа из тех (немногочисленных) что я смотрел, где бота можно держать на любом устройстве, подключенном к Интернет: не обязателен сайт/домен c ответчиком (Webhooks).


  1. andrewiWD
    20.04.2016 15:58

    Не хватает отправки изображений (чтоб без заголовков, как в StructuredMessage).


    1. pimax
      20.04.2016 23:25
      +1

      Точно. Добавил.
      Использовать можно так:

      $bot->send(new ImageMessage($message['sender']['id'], 'https://developers.facebook.com/images/devsite/fb4d_logo-2x.png'));