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

Материал является вольным переводом статьи Build A Telegram Bot with Laravel and BotMan с сайта scotch.io. 

В данном материале для создания Telegram-бота мы используем известную библиотеку BotMan (botman.io). 

На рисунке вы можете видеть результат работы созданного по этому материалу Telegram-бота. Здесь по команде /random бот вернул ссылку на случайную картинку собаки:

Естественно, кроме этой команды, бот будет понимать и другие. 

Начнем. 

В первую очередь установим Botman Studio. Если вы не знаете, что это, то знайте, Botman Studio ­– это стандартное Laravel-приложение с уже включенной в него библиотекой Botman. Так что если вы уже работали с Laravel ранее, то большинство действий из этого материала будет для вас знакомо. 

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

composer create-project --prefer-dist botman/studio ilovedogs

После того, как все установится проверьте работает ли оно. Для этого наберите в браузере: [адрес вашего сайта]/botman/tinker (например, у меня было так: site1.com/botman/tinker), и в результате вы должны увидеть следующую страницу:Здесь нас интересует первая ссылка Tinker. Перейдя по ней, вы увидите поле ввода, через которое можно «пообщаться» с ботом. Например, если ввести слово «Hi», в ответ вы получите «Hello!».Проверили, что все работает, можем продолжить. 

В этом проекте нам нужно, чтобы бот отвечал на следующие типы команд:

  • запрос ссылки на случайную картинку из всех пород собак (команда /random)
  • запрос ссылки на случайную картинку с указанием породы (например, /b dachshund)
  • запрос ссылки на случайную картинку с указанием породы и подпороды (например, /s hound:afghan)

Также добавим простую возможность диалога, где можно выбрать желаемое действие из нескольких вариантов (команда Start conversation):Кроме возможности диалога, нужно еще предусмотреть вариант, когда человек будет вводить какие-то неизвестные команды:Это весь функционал, который нам предстоит реализовать. Читайте дальше, и вы узнаете, как это сделать. 

Запрос ссылки на случайную картинку из всех пород собак (команда /random)

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

Откроем файл routes/botman.php и добавим туда новую строчку:

$botman->hears('/random', 'App\Http\Controllers\AllBreedsController@random');

Далее создадим новый контроллер с помощью команды:

php artisan make:controller AllBreedsController

Его содержимое должно выглядеть следующим образом:

<?php
namespace App\Http\Controllers;
use App\Services\DogService;
use App\Http\Controllers\Controller;
class AllBreedsController extends Controller
{
    /**
     * Controller constructor
     *
     * @return void
     */
    public function __construct()
    {
        $this->photos = new DogService;
    }
    /**
     * Return a random dog image from all breeds.
     *
     * @return void
     */
    public function random($bot)
    {
        // $this->photos->random() is basically the photo URL returned from the service.
        // $bot->reply is what we will use to send a message back to the user.
        $bot->reply($this->photos->random());
    }
}

Здесь мы используем класс DogService (app/services/DogService.php), который будет отвечать за запросы к Dog API и возвращении полученной ссылки на картинку с собакой. Содержимое этого файла должно быть следующим:

<?php
namespace App\Services;
use Exception;
use GuzzleHttp\Client;
class DogService
{
    // The endpoint we will be getting a random image from.
    const RANDOM_ENDPOINT = 'https://dog.ceo/api/breeds/image/random';
    /**
     * Guzzle client.
     *
     * @var GuzzleHttp\Client
     */
    protected $client;
    /**
     * DogService constructor
     *
     * @return void
     */
    public function __construct()
    {
        $this->client = new Client;
    }
    /**
     * Fetch and return a random image from all breeds.
     *
     * @return string
     */
    public function random()
    {
        try {
            // Decode the json response.
            $response = json_decode(
                // Make an API call an return the response body.
                $this->client->get(self::RANDOM_ENDPOINT)->getBody()
            );
            // Return the image URL.
            return $response->message;
        } catch (Exception $e) {
            // If anything goes wrong, we will be sending the user this error message.
            return 'An unexpected error occurred. Please try again later.';
        }
    }
}

Запрос ссылки на случайную картинку с указанием породы (например, /b dachshund)

Добавим новую строчку в файл routes/botman.php:

$botman->hears('/b {breed}', 'App\Http\Controllers\AllBreedsController@byBreed');

Далее откроем контроллер AllBreedsController, который мы создали ранее и добавим в него новый метод:

/**
 * Return a random dog image from a given breed.
 *
 * @return void
 */
public function byBreed($bot, $name)
{
    // Because we used a wildcard in the command definition, Botman will pass it to our method.
    // Again, we let the service class handle the API call and we reply with the result we get back.
    $bot->reply($this->photos->byBreed($name));
}

Определим используемый здесь метод byBreed() в сервисном классе DogService, который мы создали ранее:

/**
 * Fetch and return a random image from a given breed.
 *
 * @param string $breed
 * @return string
 */
public function byBreed($breed)
{
    try {
        // We replace %s    in our endpoint with the given breed name.
        $endpoint = sprintf(self::BREED_ENDPOINT, $breed);
        $response = json_decode(
            $this->client->get($endpoint)->getBody()
        );
        return $response->message;
    } catch (Exception $e) {
        return "Sorry I couldn\"t get you any photos from $breed. Please try with a different breed.";
    }
}

Также не забудьте добавить константу с api-ссылкой для получения картинки по названию породы в начало класса DogService:

// The endpoint we will hit to get a random image by a given breed name.
const BREED_ENDPOINT = 'https://dog.ceo/api/breed/%s/images/random';

Запрос ссылки на случайную картинку с указанием породы и подпороды (например, /s hound:afghan)

Добавим новую строчку в файл routes/botman.php:

$botman->hears('/s {breed}:{subBreed}', 'App\Http\Controllers\SubBreedController@random');

Далее создадим новый контроллер с помощью команды:

php artisan make:controller SubBreedController

Его содержимое должно быть следующим:

<?php
namespace App\Http\Controllers;
use App\Services\DogService;
use App\Http\Controllers\Controller;
class SubBreedController extends Controller
{
    /**
     * Controller constructor
     *
     * @return void
     */
    public function __construct()
    {
        $this->photos = new DogService;
    }
    /**
     * Return a random dog image from all breeds.
     *
     * @return void
     */
    public function random($bot, $breed, $subBreed)
    {
        $bot->reply($this->photos->bySubBreed($breed, $subBreed));
    }
}

Определим используемый здесь метод bySubBreed() в сервисном классе DogService, который мы создали ранее:

/**
 * Fetch and return a random image from a given breed and its sub-breed.
 *
 * @param string $breed
 * @param string $subBreed
 * @return string
 */
public function bySubBreed($breed, $subBreed)
{
    try {
        $endpoint = sprintf(self::SUB_BREED_ENDPOINT, $breed, $subBreed);
        $response = json_decode(
            $this->client->get($endpoint)->getBody()
        );
        return $response->message;
    } catch (Exception $e) {
        return "Sorry I couldn\"t get you any photos from $breed. Please try with a different breed.";
    }
}

Также не забудьте добавить константу с api-ссылкой для получения картинки по названию породы и подпороды в начало DogService:

// The endpoint we will hit to get a random image by a given breed name and its sub-breed.
const SUB_BREED_ENDPOINT = 'https://dog.ceo/api/breed/%s/%s/images/random';

Диалог с выбором желаемого действия (команда Start conversation)

В файле routes/botman.php уже есть строчка, отвечающая за начало диалога. Найдите метод hears() с параметром ‘Start conversation’ и замените второй параметр контроллера на следующий: ‘App\Http\Controllers\ConversationController@index’. У вас должно получиться так:

$botman->hears('Start conversation', 'App\Http\Controllers\ConversationController@index');

Далее, с помощью artisan-команды создайте новый контроллер:

php artisan make:controller ConversationController

Его содержимое должно быть следующим:

<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Conversations\DefaultConversation;
class ConversationController extends Controller
{
    /**
     * Create a new conversation.
     *
     * @return void
     */
    public function index($bot)
    {
        // We use the startConversation method provided by botman to start a new conversation and pass
        // our conversation class as a param to it. 
        $bot->startConversation(new DefaultConversation);
    }
}

Здесь мы используем класс DefaultConversation, файл для которого нужно создать в папке app/Conversations. Эта папка уже должна быть в структуре проекта, поэтому остается только создать в ней новый файл DefaultConversation.php. Его содержимое должно быть следующим:

<?php
namespace App\Conversations;
use BotMan\BotMan\Messages\Incoming\Answer;
use BotMan\BotMan\Messages\Outgoing\Question;
use BotMan\BotMan\Messages\Outgoing\Actions\Button;
use BotMan\BotMan\Messages\Conversations\Conversation;
class DefaultConversation extends Conversation
{
    /**
     * First question to start the conversation.
     *
     * @return void
     */
    public function defaultQuestion()
    {
        // We first create our question and set the options and their values.
        $question = Question::create('Huh - you woke me up. What do you need?')
            ->addButtons([
                Button::create('Random dog photo')->value('random'),
                Button::create('A photo by breed')->value('breed'),
                Button::create('A photo by sub-breed')->value('sub-breed'),
            ]);
        // We ask our user the question.
        return $this->ask($question, function (Answer $answer) {
            // Did the user click on an option or entered a text?
            if ($answer->isInteractiveMessageReply()) {
                // We compare the answer to our pre-defined ones and respond accordingly.
                switch ($answer->getValue()) {
                case 'random':
                    $this->say((new App\Services\DogService)->random());
                    break;
                    case 'breed':
                        $this->askForBreedName();
                        break;
                    case 'sub-breed':
                        $this->askForSubBreed();
                        break;
                }
            }
        });
    }
    /**
     * Ask for the breed name and send the image.
     *
     * @return void
     */
    public function askForBreedName()
    {
        $this->ask('What\'s the breed name?', function (Answer $answer) {
            $name = $answer->getText();
            $this->say((new App\Services\DogService)->byBreed($name));
        });
    }
    /**
     * Ask for the breed name and send the image.
     *
     * @return void
     */
    public function askForSubBreed()
    {
        $this->ask('What\'s the breed and sub-breed names? ex:hound:afghan', function (Answer $answer) {
            $answer = explode(':', $answer->getText());
            $this->say((new App\Services\DogService)->bySubBreed($answer[0], $answer[1]));
        });
    }
    /**
     * Start the conversation
     *
     * @return void
     */
    public function run()
    {
        // This is the boot method, it's what will be excuted first.
        $this->defaultQuestion();
    }
}

Неизвестные команды

Осталось предусмотреть вариант, когда пользователь вводит какие-то неизвестные команды, которые бот не знает. Снова откроем файл routes/botman.php и добавим новую строчку:

$botman->fallback('App\Http\Controllers\FallbackController@index');

Создадим новый контроллер командой:

php artisan make:controller FallbackController

Его содержимое должно быть следующим:

<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
class FallbackController extends Controller
{
    /**
     * Respond with a generic message.
     *
     * @param Botman $bot
     * @return void
     */
    public function index($bot)
    {
        $bot->reply('Sorry, I did not understand these commands. Try: \'Start Conversation\'');
    }
}

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

Установка Telegram-драйвера

В первую очередь нужно установить Telegram-драйвер в проект:

composer require botman/driver-telegram

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

Открываем Telegram и через поиск ищем BotFather. После чего пишем: /newbot. Далее выбираем имя для бота (name), я использовал имя BotmanTest. Затем выбираем пользовательское имя (username), оно должно быть уникальным. После этого вы получите api-токен, который нужно указать в файле .env проекта, для этого добавьте следующую строчку в конец файла:

TELEGRAM_TOKEN=YOUR_TOKEN

И замените YOUR_TOKEN на полученный api-токен. 

Открываем доступ извне к проекту с помощью ngrok 

Чтобы не выкладывать свой тестовый проект на отдельный сервер в сети, воспользуемся утилитой ngrok. С помощью этой утилиты ваш проект на localhost станет доступен по отдельному адресу в Интернете. Если у вас еще не установлен ngrok, используйте официальную страницу (https://ngrok.com/download) для установки. 

После того, как вы установите ngrok запустите Laravel-сервер:

php artisan serve

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

./ngrok http 8000

Теперь ваш проект будет доступен из сети по адресам, которые указаны в строчках Forwarding:Для работы Telegram-бота нужен https-адрес, поэтому используйте его.

Связываем наш проект с Telegram

Для того, чтобы связать наш проект с Telegram, используйте Postman или CURL для выполнения следующей команды:

curl -X POST -F 'url=https://{YOU_URL}/botman' https://api.telegram.org/bot{TOKEN}/setWebhook

YOU_URL – https-адрес из ngrok; TOKEN – это TELEGRAM_TOKEN, который вы указали ранее в файле .env. 

Если вы все сделали правильно, то команда должна вернуть следующий результат:

{
    "ok": true,
    "result": true,
    "description": "Webhook was set"
}

На этом все, результат работы в Telegram приведен на картинке: