Telegram бот на Laravel #6: контроллер для обработки запросов — PROG-TIME

Telegram бот на Laravel #6: контроллер для обработки запросов

26.06.2024
Содержание:

В шестом уроке я покажу вам контроллер для обработки запросов от Телеграм бота. В данной записи мы с вами разберём контроллер TelegramBot.

Настройка роутинга

Необходимо в директории routes создать файл api.php.

Новый файл необходимо указать в файле bootstrap/app.php в блоке withRouting. Файл должен выглядеть так:

use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        api: __DIR__.'/../routes/api.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware) {
        //
    })
    ->withExceptions(function (Exceptions $exceptions) {
        //
    })->create();

В файле api.php необходимо прописать следующий код

use App\Http\Controllers\TelegramBot;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

Route::controller(TelegramBot::class)->group(function () {
    Route::post('/telegram/bot', 'botController');
});

Настройка хуков от Telegram

Для получения запросов от Telegram нам необходимо отправить запрос на подключение хука к адресу «домен/api/telegram/bot«.

Для того сгенерируйте ссылку и откройте её в браузере

https://api.telegram.org/bot{токен}/setWebhook?url={путь_до_обработчика}

Контроллер для обработки запросов от Telegram бота

namespace App\Http\Controllers;

use App\Models\Article;
use App\Models\BotUser;
use App\Models\Chapter;

use App\Models\Favorite;
use App\Models\ProgressStudy;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;

class TelegramBot extends Controller
{
    private array $dataQuery;
    private array $dataMessage;
    private int $chatID;
    private int $messageID;
    private mixed $callbackID;
    private string $dataValue;
    private mixed $progressStudy;
    private mixed $favoriteData;
    private object $userParams;
    private $lastMessageID;

    public function botController()
    {
        $dataQuery = file_get_contents('php://input');
        // Log::debug($dataQuery);

        if (!empty($dataQuery)) {
            $this->dataQuery = json_decode($dataQuery, true);

            if (!empty($this->dataQuery['message'])) {
                $this->dataMessage = $this->dataQuery['message'];

                $this->chatID = (int)$this->dataQuery['message']['chat']['id'];
                $this->messageID = (int)$this->dataQuery['message']['message_id'];

            } elseif (!empty($this->dataQuery['callback_query'])) {
                $this->dataMessage = $this->dataQuery['callback_query']['message'];
                $this->callbackID = (int)$this->dataQuery['callback_query']['id'];

                $this->chatID = (int)$this->dataMessage['chat']['id'];
                $this->messageID = (int)$this->dataMessage['message_id'];

                $this->dataValue = $this->dataQuery['callback_query']['data'];
            }

            if (!empty($this->chatID)) {
                $this->progressStudy = ProgressStudy::getProgressStudy($this->chatID);
                if (empty($this->progressStudy)) {
                    ProgressStudy::create(
                        [
                            'id_telegram' => $this->chatID,
                            'progress_value' => json_encode([]),
                            'updated_at' => date('d.m.Y H:i'),
                        ]
                    );
                    $this->progressStudy = ProgressStudy::getProgressStudy($this->chatID);
                }

                $this->favoriteData = Favorite::getFavorite($this->chatID);
                if (empty($this->favoriteData)) {
                    Favorite::create(
                        [
                            'id_telegram' => $this->chatID,
                            'favorite_value' => json_encode([]),
                            'updated_at' => date('d.m.Y H:i'),
                        ]
                    );
                    $this->favoriteData = Favorite::getFavorite($this->chatID);
                }

                $userParams = [
                    'id_telegram' => $this->dataMessage['chat']['id'],
                    'firstname' => $this->dataMessage['chat']['first_name'] ?? "",
                    'lastname' => $this->dataMessage['chat']['last_name'] ?? "",
                    'username' => $this->dataMessage['chat']['username'] ?? "",
                ];
                $this->userParams = BotUser::changeUser((int)$this->chatID, $userParams);

                $this->lastMessageID = $this->userParams->id_last_message;

                if (!empty($this->dataQuery['message'])) {
                    $this->messageController();
                } elseif (!empty($this->dataQuery['callback_query'])) {
                    $this->callbackController();
                }
            }
        }

        $this->end();

        die();
    }

    /**
     *
     * @return void
     */
    public function end(): void
    {
        if (!empty($this->lastMessageID)) {
            $lastMessageID = $this->lastMessageID < $this->messageID ? $this->messageID : $this->lastMessageID;
            BotUser::where('id_telegram', $this->chatID)->update([
                'id_last_message' => $lastMessageID,
            ]);
        }
    }

    /**
     * Отправка запроса
     *
     * @param string $method
     * @param array $dataQuery
     * @return void
     */
    private function sendQuery(string $method, array $dataQuery): void
    {
        $resultQuery = TelegramMethods::sendQueryTelegram($method, $dataQuery);
        if (!empty($resultQuery['ok']) && $method !== 'deleteMessage') {
            if (!empty($resultQuery['result']['message_id'])) {
                $this->lastMessageID = $resultQuery['result']['message_id'];
            }
        }
    }

    /**
     * Отправка сообщений со списком глав конституции
     *
     * @return array
     */
    private function listChaptersData(): array
    {
        $listChapters = Chapter::select('*')
            ->orderBy('id')
            ->get();

        $index = 1;
        $keyboardData = [];
        $arrayRow = [];
        foreach ($listChapters as $article) {
            $arrayRow[] = [
                'text' => 'Глава №' . $article->id,
                'callback_data' => 'chapter_' . $article->id,
            ];

            if ($index % 2 == 0) {
                $keyboardData[] = $arrayRow;
                $arrayRow = [];
            }
            if ($index == count($listChapters)) {
                $keyboardData[] = $arrayRow;
            }
            $index++;
        }

        $keyboardData[] = [
            [
                'text' => '🔙 Назад',
                'callback_data' => 'start_menu',
            ]
        ];

        $messageParams = [
            'method' => 'editMessageText',
            'dataQuery' => [
                'chat_id' => $this->chatID,
                'message_id' => $this->messageID,
                'text' => 'Список глав',
                'reply_markup' => json_encode([
                    'inline_keyboard' => $keyboardData
                ]),
            ]
        ];

        return $messageParams;
    }

    /**
     * Список статей добавленных в "Избранное"
     *
     * @return array
     */
    private function listFavoritesData(): array
    {
        $listFavorites = Favorite::getFavorite($this->chatID);
        $favoriteValue = $listFavorites->favorite_value;

        if (!empty($favoriteValue)) {
            $listArticles = Article::select('*')
                ->whereIn('article', $favoriteValue)
                ->get();

            $keyboardData = $this->templateKeyboardArticles($listArticles, 'favorite');

            $keyboardData[] = [
                [
                    'text' => '🔙 Назад',
                    'callback_data' => 'start_menu',
                ]
            ];

            $messageParams = [
                'method' => 'editMessageText',
                'dataQuery' => [
                    'chat_id' => $this->chatID,
                    'message_id' => $this->messageID,
                    'text' => 'Список статей добавленных в "Избранное"',
                    'reply_markup' => json_encode([
                        'inline_keyboard' => $keyboardData
                    ]),
                ]
            ];
        } else {
            $keyboardData[] = [
                [
                    'text' => '🔙 Назад',
                    'callback_data' => 'start_menu',
                ]
            ];
            $messageParams = [
                'method' => 'editMessageText',
                'dataQuery' => [
                    'chat_id' => $this->chatID,
                    'message_id' => $this->messageID,
                    'text' => 'Раздел "🌟 Избранное" пуст!',
                    'reply_markup' => json_encode([
                        'inline_keyboard' => $keyboardData
                    ]),
                ]
            ];
        }

        return $messageParams;
    }

    /**
     * Отправка сообщения с текстом статьи
     *
     * @param int $articleID
     * @param string $codeButBack
     * @return array
     */
    private function articleData(int $articleID, string $codeButBack = ""): array
    {
        $articleData = Article::select('*')
            ->where('article', $articleID)
            ->first();

        $messageParams = [];
        if (!empty($articleData)) {
            if (in_array($articleID, $this->progressStudy['progress_value'])) {
                $readButText = "✅  Прочитано";
            } else {
                $readButText = "☑️  Прочитано";
            }
            $keyboardData[] = [
                [
                    'text' => $readButText,
                    'callback_data' => 'set_status_read_' . $articleData->article,
                ]
            ];

            if (in_array($articleID, $this->favoriteData['favorite_value'])) {
                $favoriteButText = "⭐️ Добавлено в избранное";
            } else {
                $favoriteButText = "Добавить в избранное";
            }
            $keyboardData[] = [
                [
                    'text' => $favoriteButText,
                    'callback_data' => 'set_status_favorite_' . $articleData->article,
                ]
            ];

            if ((int)$articleData->article > 1) {
                $prevID = (int)$articleData->article - 1;
                $rowNavigation[] = [
                    'text' => '⬅️',
                    'callback_data' => 'article_' . $prevID,
                ];
            }

            if ((int)$articleData->article < 137) {
                $nextID = (int)$articleData->article + 1;
                $rowNavigation[] = [
                    'text' => '➡️',
                    'callback_data' => 'article_' . $nextID,
                ];
            }

            $keyboardData[] = $rowNavigation;

            if (empty($codeButBack)) {
                $codeButBack = 'chapter_' . $articleData->chapter;
            }

            $keyboardData[] = [
                [
                    'text' => '🔙 Назад',
                    'callback_data' => $codeButBack,
                ]
            ];

            $textMessage = "Статья №" . $articleData->article;
            $textMessage .= "\n\n";
            $textMessage .= $articleData->text;

            $messageParams = [
                'method' => 'editMessageText',
                'dataQuery' => [
                    'chat_id' => $this->chatID,
                    'message_id' => $this->messageID,
                    'text' => $textMessage,
                    'reply_markup' => json_encode([
                        'inline_keyboard' => $keyboardData
                    ]),
                ]
            ];
        }

        return $messageParams;
    }

    /**
     * Изменение статуса прочтения статьи
     *
     * @param int $articleID
     * @return void
     */
    private function setReadArticle(int $articleID): void
    {
        $progressStudy = ProgressStudy::changeProgressStudy((int)$this->chatID, (int)$articleID);

        if (empty($progressStudy->progress_value)) {
            $progressStudy->progress_value = [];
        }

        $this->progressStudy['progress_value'] = $progressStudy->progress_value;

        $queryParams = $this->articleData($articleID);
        if (!empty($queryParams)) {
            $this->sendQuery('editMessageText', $queryParams['dataQuery']);
        }

        $callbackParams = [
            'callback_query_id' => $this->callbackID,
            'chat_id' => $this->chatID,
        ];
        if (in_array($articleID, $this->progressStudy['progress_value'])) {
            $callbackParams['text'] = 'Статья добавлена в прочитанное';
        } else {
            $callbackParams['text'] = 'Статья убрана из прочитанного';
        }
        $this->sendQuery('answerCallbackQuery', $callbackParams);
    }

    /**
     * Изменение статуса "Избранное" статьи
     *
     * @param int $articleID
     * @return void
     */
    private function setFavoriteArticle(int $articleID)
    {
        $favoriteData = Favorite::changeFavorite((int)$this->chatID, (int)$articleID);

        if (!empty($favoriteData)) {
            $this->favoriteData['favorite_value'] = $favoriteData->favorite_value;

            $queryParams = $this->articleData($articleID);
            if (!empty($queryParams)) {
                $this->sendQuery('editMessageText', $queryParams['dataQuery']);
            }

            $callbackParams = [
                'callback_query_id' => $this->callbackID,
                'chat_id' => $this->chatID,
            ];
            if (in_array($articleID, $this->favoriteData['favorite_value'])) {
                $callbackParams['text'] = 'Добавлено в "избранное"';
            } else {
                $callbackParams['text'] = 'Удалено из раздела "избранное"';
            }
            $this->sendQuery('answerCallbackQuery', $callbackParams);
        }
    }

    /**
     * Формирование клавиатуры для сообщения со статьёй
     *
     * @param object $listArticles - список статей
     * @param string $postfixArticleCode - добавление code к статье
     * @return array
     */
    private function templateKeyboardArticles(object $listArticles, string $postfixArticleCode = ""): array
    {
        $index = 1;
        $keyboardData = [];
        $arrayRow = [];
        foreach ($listArticles as $article) {
            if (in_array($article->article, $this->progressStudy['progress_value'])) {
                $articleButText = "✅ " . $article->article;
            } else {
                $articleButText = "☑️ " . $article->article;
            }

            $arrayRow[] = [
                'text' => $articleButText,
                'callback_data' => 'article_' . $article->id . ((!empty($postfixArticleCode)) ? '_' . $postfixArticleCode : ""),
            ];

            if ($index % 4 == 0) {
                $keyboardData[] = $arrayRow;
                $arrayRow = [];
            }
            if ($index == count($listArticles)) {
                $keyboardData[] = $arrayRow;
            }
            $index++;
        }

        return $keyboardData;
    }

    /**
     * Список статей
     *
     * @param int $chapterID
     * @return array
     */
    private function listArticlesData(int $chapterID): array
    {
        $listArticles = Article::select('*')
            ->where('chapter', $chapterID)
            ->orderBy('id')
            ->get();

        $keyboardData = $this->templateKeyboardArticles($listArticles);

        $keyboardData[] = [
            [
                'text' => '🔙 Назад',
                'callback_data' => 'chapters',
            ]
        ];

        $messageParams = [
            'method' => 'editMessageText',
            'dataQuery' => [
                'chat_id' => $this->chatID,
                'message_id' => $this->messageID,
                'text' => 'Глава №' . $chapterID . ': список статей',
                'reply_markup' => json_encode([
                    'inline_keyboard' => $keyboardData
                ]),
            ]
        ];

        return $messageParams;
    }

    /**
     * Изменение статуса "Прочитано"
     *
     * @return void
     */
    private function changeStatusStudy(): void
    {
        if (!empty($this->userParams->study_status)) {
            $newStatus = 0;
        } else {
            $newStatus = 1;
        }

        $userParams = [
            'study_status' => $newStatus,
        ];
        $this->userParams = BotUser::changeUser((int)$this->chatID, $userParams);

        $queryParams = $this->getStudyMenu();
        $this->sendQuery('editMessageText', $queryParams);
    }

    /**
     * Генерация сообщения при нажатии на "Прочитано"
     *
     * @return array
     */
    private function getStudyMenu(): array
    {
        $textMessage = $this->textMyProgress();
        $textStatusStudy = (!empty($this->userParams->study_status)) ? '⛔️ Закончить обучение' : '👨🏼‍🎓 Начать обучение';
        $messageParams = [
            'chat_id' => $this->chatID,
            'message_id' => $this->messageID,
            'parse_mode' => 'html',
            'text' => $textMessage,
            'reply_markup' => json_encode([
                'inline_keyboard' => [
                    [
                        [
                            'text' => $textStatusStudy,
                            'callback_data' => 'study_change_status',
                        ],
                    ],
                    [
                        [
                            'text' => '🔙 Назад',
                            'callback_data' => 'start_menu',
                        ],
                    ],
                ],
            ]),
        ];

        return $messageParams;
    }

    /**
     * Текст сообщения прогресса обучения
     *
     * @return string
     */
    private function textMyProgress(): string
    {
        $textMessage = "<b>🎓 Мой прогресс обучения</b> \n";

        $progressValue = $this->progressStudy->progress_value;
        if (!empty($progressValue)) {
            $procentStudy = round(count($progressValue) / 137 * 100);

            if ($procentStudy < 20) {
                $textMessage .= "Уровень: школьник \n";
            } elseif ($procentStudy >= 20 && $procentStudy < 50) {
                $textMessage .= "Уровень: понимающий \n";
            } elseif ($procentStudy >= 50 && $procentStudy < 100) {
                $textMessage .= "Уровень: знаток \n";
            } elseif ($procentStudy == 100) {
                $textMessage .= "Уровень: знаток \n";
            }

            $textMessage .= "Прогресс: " . count($progressValue) . "/137 статей \n";
        } else {
            $textMessage .= "Уровень: школьник \n";
            $textMessage .= "Прогресс: 0/137 статей \n";
        }

        $textMessage .= "\n";
        $textMessage .= "<b>📖 Начать обучение</b>: \n";
        $textMessage .= "Бот будет присылать новые статьи на изучение каждый день";

        return $textMessage;
    }

    /**
     * Обработка нажатия на команды
     *
     * @return void
     */
    private function callbackController(): void
    {
        if (!empty($this->dataQuery)) {
            $arParams = explode('_', $this->dataValue);

            switch ($this->dataValue) {
                /* список глав */
                case 'start_menu':
                    $this->sendQuery('editMessageText', $this->getStartMenu(true));
                    break;

                case 'study_change_status':
                    $this->changeStatusStudy();
                    break;

                case 'chapters':
                    $queryParams[] = $this->listChaptersData();
                    break;

                case 'favorites':
                    $queryParams[] = $this->listFavoritesData();
                    break;

                case 'study':
                    $this->sendQuery('editMessageText', $this->getStudyMenu());
                    break;

                case preg_match('/(chapter_)/', $this->dataValue) == 1:
                    $idChapter = $arParams[1];

                    $queryParams[] = $this->listArticlesData((int)$idChapter);

                    break;

                case preg_match('/(article_)/', $this->dataValue) == 1:
                    $idArticle = $arParams[1];

                    $codeBut = "";
                    if (!empty($arParams[2])) {
                        $dopCodeArticle = $arParams[2];
                        $codeBut = ($dopCodeArticle == 'favorite') ? 'favorites' : '';
                    }

                    $queryParams[] = $this->articleData($idArticle, $codeBut);

                    break;

                case preg_match('/(set_status_read_)/', $this->dataValue) == 1:
                    $idArticle = $arParams[3];
                    $this->setReadArticle((int)$idArticle);

                    break;

                case preg_match('/(set_status_favorite_)/', $this->dataValue) == 1:
                    $idArticle = $arParams[3];
                    $this->setFavoriteArticle($idArticle);

                    break;
            }

            if (!empty($queryParams)) {
                foreach ($queryParams as $param) {
                    if (!empty($param['method']) && !empty($param['dataQuery'])) {
                        $this->sendQuery($param['method'], $param['dataQuery']);
                    }
                }
            }

        }
    }

    /**
     * Генерация сообщения для команды help
     *
     * @return array
     */
    private function getHelpMessage(): array
    {
        $textMessage = "*Базовые команды для работы с ботом*: \n\n";
        $textMessage .= "▶️ /start – запуск/перезапуск бота \n\n";
        $textMessage .= "▶️ /help – описание команд \n\n";
        $textMessage .= "📃 /articles – показать список разделов бота \n\n";
        $textMessage .= "📃 `/article_{номер_статьи}` – открыть статью по номеру \n\n";
        $textMessage .= "👨🏼‍🎓 /study – открыть раздел обучения \n\n";
        $textMessage .= "🌟 /favorites – открыть раздел \"Избранное\" \n\n";

        $textMessage .= "Данный проект был полностью разработан командой *Prog-Time*";

        $messageParams = [
            'chat_id' => $this->chatID,
            'text' => $textMessage,
            'parse_mode' => 'MARKDOWN',
            'message_id' => $this->messageID,
            'reply_markup' => json_encode([
                'inline_keyboard' => [
                    [
                        [
                            'text' => 'Перейти на сайт автора',
                            'url' => 'https://prog-time.ru',
                        ],
                    ],
                    [
                        [
                            'text' => '🏘 На главную',
                            'callback_data' => 'start_menu',
                        ],
                    ],
                ],
            ]),
        ];

        return $messageParams;
    }

    /**
     * Обработка текстовых команд
     *
     * @return void
     */
    private function messageController(): void
    {
        if (!empty($this->dataMessage)) {
            $this->sendQuery('deleteMessage', [
                'chat_id' => $this->chatID,
                'message_id' => $this->lastMessageID
            ]);

            switch ($this->dataMessage['text']) {
                case '/start':
                    $this->sendQuery('sendMessage', $this->getStartMenu());
                    break;

                case '/articles':
                    $queryParams = $this->listChaptersData();
                    $this->sendQuery('sendMessage', $queryParams['dataQuery']);
                    break;

                case '/favorites':
                    $queryParams = $this->listFavoritesData();
                    $this->sendQuery('sendMessage', $queryParams['dataQuery']);
                    break;

                case '/study':
                    $this->sendQuery('sendMessage', $this->getStudyMenu());
                    break;

                case '/help':
                    $this->sendQuery('sendMessage', $this->getHelpMessage());
                    break;
            }

            $this->sendQuery('deleteMessage', [
                'chat_id' => $this->chatID,
                'message_id' => $this->messageID
            ]);
        }
    }

    /**
     * Генерация стартового сообщения
     *
     * @param bool $editStatus - статус редактирования
     * @return array
     */
    private function getStartMenu(bool $editStatus = false): array
    {
        $textMessage = "С помощью данного бота вы можете изучить статьи «Конституции РФ». \n\n";
        $textMessage .= "<b>Разделы бота</b>: \n";
        $textMessage .= "📃 <b>Статьи</b> – основной раздел, где хранятся статьи разбитые на главы \n\n";
        $textMessage .= "👨🏼‍🎓 <b>Прогресс изучения</b> – информация о статусе изучения Конституции РФ \n\n";
        $textMessage .= "🌟 <b>Избранное</b> – список статей добавленных в избранное \n\n";

        $messageParams = [
            'chat_id' => $this->chatID,
            'text' => $textMessage,
            'parse_mode' => 'html',
            'reply_markup' => json_encode([
                'inline_keyboard' => [
                    [
                        [
                            'text' => '📃 Статьи',
                            'callback_data' => 'chapters',
                        ],
                    ],
                    [
                        [
                            'text' => '👨🏼‍🎓 Прогресс обучения',
                            'callback_data' => 'study',
                        ],
                    ],
                    [
                        [
                            'text' => '🌟 Избранное',
                            'callback_data' => 'favorites',
                        ],
                    ],
                ],
            ]),
        ];

        if ($editStatus) {
            $messageParams['message_id'] = $this->messageID;
        }

        return $messageParams;
    }

}

Материалы курса