Добрый день.

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

Итак, как поднимать веб-сервер и получить ssl-сертификат я рассказывать не буду, про это написано достаточно.

Создание Telegram-бота


Сначала создадим Telegram-бота, для этого идем в Telegram и находим там бота BotFather.





Выбираем /newbot



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



Следующий шаг — это сообщить серверам Telegram, на какой сервер отправлять данные от бота. Для этого делаем ссылку вида:

https: //api.telegram.org/bot___ТОКЕН___/setWebhook?url=https://____ПУТЬ_ДО_СКРПИТА___

___ТОКЕН___ заменяем на наш токен от бота, полученный ранее

____ПУТЬ_ДО_СКРПИТА___ заменяем на адрес скрипта на нашем сервере, где будет происходить обработка данных (например, www.my_server.ru/webhook_telegram.php).

Тут есть проблема, сервер api.telegram.org находится под блокировкой, но можно сделать так: арендовать самый дешевый сервер там, где нет ограничений и дать из консоли этого сервера команду

wget ___ПОЛУЧИВШИЙСЯ_АДРЕС___

Все, Telegram-бот создан и связан с вашим сервером.

Создание навыка для Яндекс.Алиса


Перейдем к созданию навыка для Яндекс.Алиса.

Для создания навыка нужно перейти на страницу разработчиков Яндекс.Диалоги страницу разработчиков Яндекс.Диалоги, нажать там «Создать диалог» и выбрать «Навык в Алисе».



Откроется диалог настроек навыка.



Начинаем вводить настройки навыка.

Вводим имя вашего навыка.



Активационное имя следует подбирать очень внимательно, чтобы Алиса правильно его понимала, из нюансов – мобильное приложение с Алисой и колонки типа Яндекс.Станция или Irbis A могут воспринимать слова по-разному.

Вводим путь до скрипта на нашем сервере так же, как и для Telegram, но это будет скрипт именно для Алисы, например www.my_server.ru/webhook_alice.php.



Выбираем голос, которым будет говорить навык, мне больше нравится именно голос Алисы.



Если планируется работа только на мобильных устройствах или в браузере, то выбираем «Нужно устройство с экраном».

Далее вводим настройки для каталога навыков Алисы. Если вы планируете использовать для активации слово — бренд, нужно пройти верификацию сайта бренда в сервисе webmaster.yandex.ru.



С настройками все, переходим к скриптам.

Скрипт Telegram-бота


Начнем со скрипта для Telegram.

Подключаем библиотеку, где будут обрабатываться сообщения от бота и Алисы:

include_once 'webhook_parse.php';

Задаем token нашего бота:

$tg_bot_token = "_____YOUR_BOT_TOKEN_____";

Получаем данные:

$request = file_get_contents('php://input');
$request = json_decode($request, TRUE);

Разбираем данные на переменные:

if (!$request)
{
  die();
    // Some Error output (request is not valid JSON)
}
else if (!isset($request['update_id']) || !isset($request['message']))
{
  die();
    // Some Error output (request has not message)
}
else
{
  $user_id = $request['message']['from']['id'];
  $msg_user_name = $request['message']['from']['first_name'];
  $msg_user_last_name = $request['message']['from']['last_name'];
  $msg_user_nick_name = $request['message']['from']['username'];
  $msg_chat_id = $request['message']['chat']['id'];
  $msg_text = $request['message']['text'];


  $msg_text = mb_strtolower($msg_text, 'UTF-8');


  $tokens = explode(" ", $msg_text);
}

Теперь можно работать с переменными:

$tokens — тут теперь все слова, которые пользователь ввел

$user_id — тут id пользователя

$msg_chat_id — чат, в котором бот получил команду

$msg_user_name — имя пользователя

Далее, вызываем для обработки функцию Parse_Tokens:

$Out_Str = Parse_Tokens($tokens);

И отправляем ответ:

Send_Out($user_id, $Out_Str);

функция Send_Out проста и выглядит так:

function Send_Out($user_id, $text, $is_end = true)
{
  global $tg_bot_token;
  if (strlen($user_id) < 1 || strlen($text) < 1) {return;}
  $json = file_get_contents('https://api.telegram.org/bot' . $tg_bot_token . '/sendMessage?chat_id=' . $user_id . '&text=' . $text);
}

Скрипт навыка для Яндекс.Алиса


Теперь перейдем к скрипту для Алисы, он почти такой же, как и для Telegram.

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

include_once 'classes_alice.php';
include_once 'webhook_parse.php';

Получаем данные:

$data = json_decode(trim(file_get_contents('php://input')), true);

Разбираем данные на переменные:

if (isset($data['request']))
{

//original_utterance


  if (isset($data['meta']))
  {
    $data_meta = $data['meta'];
    if (isset($data_meta['client_id'])) {$client_id = $data_meta['client_id'];}
  }

  if (isset($data['request']))
  {
    $data_req = $data['request'];

    if (isset($data_req['original_utterance']))
    {
      $original_utterance = $data_req['original_utterance'];
    }


    if (isset($data_req['command'])) {$data_msg = $data_req['command'];}
    if (isset($data_req['nlu']))
    {
      $data_nlu = $data_req['nlu'];
      if (isset($data_nlu['tokens'])) {$tokens = $data_nlu['tokens'];}
//      $data_token_count = count($data_tokens);
    }
  }
  if (isset($data['session']))
  {
    $data_session = $data['session'];
    if (isset($data_session['new'])) {$data_msg_new = $data_session['new'];}
    if (isset($data_session['message_id'])) {$data_msg_id = $data_session['message_id'];}
    if (isset($data_session['session_id'])) {$data_msg_sess_id = $data_session['session_id'];}
    if (isset($data_session['skill_id'])) {$skill_id = $data_session['skill_id'];}
    if (isset($data_session['user_id'])) {$user_id = $data_session['user_id'];}
  }
}

Тут нужных переменных чуть меньше:

$tokens — тут теперь все слова, которые пользователь ввел

$user_id — тут id пользователя

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

  if (strpos($tokens[0], "ping") > -1)     {Send_Out("pong", "", true);}

Вызываем для обработки функцию Parse_Tokens, он тот же самый, что и для Telegram:

$Out_Str = Parse_Tokens($tokens);

И отправляем ответ:

Send_Out($user_id, $Out_Str);

Функция Send_Out тут посложней:

function Send_Out($user_id, $out_text, $out_tts = "", $is_end = false)
{
  global $data_msg_sess_id, $user_id;

  ///// GENERATE BASE OF OUT //////
    $Data_Out = new Alice_Data_Out();
    $Data_Out->response = new Alice_Response();
    $Data_Out->session = new Alice_Session();
  ///// GENERATE BASE OF OUT End //////

  ///// OUT MSG GENERATE /////
  $Data_Out->session->session_id = $data_msg_sess_id;;
  $Data_Out->session->user_id = $user_id;

  $Data_Out->response->text = $out_text;
  $Data_Out->response->tts = $out_tts;

  if (strlen($out_tts) < 1) {$Data_Out->response->tts = $out_text;}

  $Data_Out->response->end_session = $is_end;

  header('Content-Type: application/json');
  print(json_encode($Data_Out, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT));

  die();
}

Закончили скрипт для Алисы.

Сам скрипт обработки Parse_Tokens сделал чисто для примера, вы сможете сделать там любые проверки и обработки.

function Parse_Tokens($tokens)
{
  $out = "";
  // do something with tokens //
  $out =  "Your eneter " . count($tokens) . " words: " . implode($tokens, " ");
  return $out;
}

Если вам нужно общение с пользователем более сложного вида, чем вопрос-ответ, то нужно будет сохранять в базе (например mysql) $user_id пользователя и уже полученные от пользователя данные и анализировать их в функции Parse_Tokens.

Собственно это почти все, если все сделано правильно, то Telegram-бот уже доступен, навык Алисы можно проверить dialogs.yandex.ru/developer, перейдя в свой новый навык на закладку тестирование.



Если все корректно работает, можно отправить навык на модерацию, нажав кнопку «На модерацию».

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

Документация по сервису Яндекс.Диалоги тут

Полные скрипты выложены на github скачать.

Обновление: завернул все в классы и обновил репозитарий на github

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


  1. Skycaptain
    01.04.2019 05:54

    Из какого вы года, путешественник во времени?


  1. evgwed
    01.04.2019 07:00

    Задумка статьи интересная, спасибо!

    За такое нужно руки отрывать в 2к19

    global $tg_bot_token;


    1. KillerAngel Автор
      01.04.2019 08:24

      Всегда пожалуйста.


    1. iDm1
      01.04.2019 13:13
      -1

      Если своим сообщением вы хотите сделать мир лучше, то правильно было бы сразу привести примет верного подхода в данном случае.
      А так выглядит как хвастовство: «Смотрите какой-то говнокодер, не то что я! :)»


      1. evgwed
        01.04.2019 13:24
        +1

        Не думал, что нужно пояснять такие очевидные вещи.

        function Send_Out($user_id, $text, $is_end = true)
        {
          global $tg_bot_token;
          if (strlen($user_id) < 1 || strlen($text) < 1) {return;}
          $json = file_get_contents('https://api.telegram.org/bot' . $tg_bot_token . '/sendMessage?chat_id=' . $user_id . '&text=' . $text);
        }


        Этот код можно решить в несколько путей:

        1. В функции добавить еще один входной параметр в функцию:
        function Send_Out($tg_bot_token, $user_id, $text, $is_end = true)
        {
          if (strlen($user_id) < 1 || strlen($text) < 1) {return;}
          $json = file_get_contents('https://api.telegram.org/bot' . $tg_bot_token . '/sendMessage?chat_id=' . $user_id . '&text=' . $text);
        }


        2. Использовать классы и создать класс похожего типа:
        class TelegramClient 
        {
            private $botToken;
            
            function __construct(string $botToken) 
            {
                $this->botToken = $botToken;
            }
            
            public function sendMessage(int $userId, string $message, bool $isEnd = true): void
            {
                if ($userId < 1 || mb_strlen($message) < 1) {
                    throw new TelegramClientException(...);
                }
                $json = file_get_contents('https://api.telegram.org/bot' . $tg_bot_token . '/sendMessage?chat_id=' . $user_id . '&text=' . $text);
            }
        }

        Запросы на сервер не нужно отправлять через file_get_contents, лучше использовать Guzzle.


        1. KillerAngel Автор
          01.04.2019 16:11
          -1

          А можно все завернуть в классы и сделать так:

            include_once 'webhook_class.php';
          
            $tg_bot_token = "_____YOUR_BOT_TOKEN_____";
          
            $Telegram_Cli = new Telegram_Cli_Class($tg_bot_token);
          
            $Webhook = new Webhook_Class();
          
            $Webhook->Set_Type('telegram');
            $Webhook->Get_Data();
            $Webhook->Parse_Data();
            $Webhook->Parse_Tokens();
            if ($Webhook->is_Out())
            {
              $Telegram_Cli->Send($Webhook->user_id, $Webhook->out_msg);
            }
          }
          


  1. 2PAE
    01.04.2019 08:16

    Готовые примеры для чего это может быть полезно, где-то есть посмотреть?


    1. KillerAngel Автор
      01.04.2019 08:20

      В открытом виде нет, используется для управление домашней автоматикой, к аккаунту пользователя привязывается id от Алисы и от Telegram. Команды управление получаются одинаковые.


  1. zhulan0v
    01.04.2019 08:16

    Код конечно печальный, но плюсанул за актуальную тематику. Если б не она, то вас бы слили (может еще и сольют).


    1. KillerAngel Автор
      01.04.2019 08:23

      Код примитивный, кто умеет красиво и сам разберется за час.


      1. zhulan0v
        01.04.2019 08:26

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


  1. 0ri0n
    01.04.2019 08:48

    Не знаю что Вы… до парня на счет кода. Как смог так написал. (Я уверен, что если сделаете более читаемый код, он не откажется и опубликует и его.)
    Благодарю за статью, ты заинтересовал меня в возможностях «Алисы».


  1. win0err
    01.04.2019 10:57

    Да что вы прикопались к коду?
    Нормальный первоапрельский код.


    Ну а если серьёзно, KillerAngel, не используйте глобальные переменные.
    Почитайте про чистые функции и почему нужно стремиться писать именно чистые функции.
    Почитайте про ООП, с помощью него можно лучше структурировать код.
    И не забывайте про многобайтовые кодировки, вы же с PHP работаете (mb_strlen, например).


    1. KillerAngel Автор
      01.04.2019 16:23
      -1

      А я и не использую, ну почти.
      Про ООП читал.
      Про кодировки — да, когда срезал всю шелуху, упустил strlen

      Все переделал, теперь все вот так, что один что другой скрипт:

        include_once 'webhook_class.php';
      
        $Yandex_Alice_Cli = new Yandex_Alice_Cli_Class();
      
        $Webhook = new Webhook_Class();
      
        $Webhook->Set_Type('yandex_alice');
        $Webhook->Get_Data();
        $Webhook->Parse_Data();
        $Webhook->Parse_Tokens();
        if ($Webhook->is_Out())
        {
          $Yandex_Alice_Cli->Set_Sess_Id($Webhook->data_msg_sess_id);
          $Yandex_Alice_Cli->Send($Webhook->user_id, $Webhook->out_msg);
        }
      


      1. Roman_S1
        02.04.2019 15:24

        Это, конечно, лучше чем было, но ведь есть PSR и код оформлять желательно в соответствии с ним


        1. KillerAngel Автор
          02.04.2019 15:38

          Согласен


  1. Disinterpreter
    02.04.2019 15:24

    Идея хорошая, кому не нравится, сделайте лучше.

    А вообще, у меня была подобная проблема, когда я писал единого бота для VK&Discord&LINE.

    Однако, там я решил эту проблему, написав несколько «драйверов», которые формировали единый JSON-массив и отправляли его в RabbitMQ, а на стороне считывания уже работал с этими стандартизированными данными. Так что, вам есть к чему стремиться.


    1. KillerAngel Автор
      02.04.2019 15:36

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