
Финальная часть разработки простого проекта про specialty-кофейни на Кипре. В первой части я рассказал про API микросервис, во второй — про фронтэнд-сайт и теперь — про телеграм-бота.
Изначально перед ботом ставились простые задачи:
-
/map — карта кофеен
-
/list — список кофеен
-
подробности о кофейне
-
/random — случайная кофейня
-
поиск кофейни по названию
-
поиск ближайшей кофейни по своему местоположению или по команде /nearest
В ходе реализации выяснилось, что Telegram не умеет показывать встроенную карту с несколькими маркерами, а также не отправляет местоположение в веб-версии. Поэтому вместо карты пришлось показывать ссылку на сайт с картой, а при вместо ответа при получении пустого местоположения — сообщение-заглушку. Все остальное удалось реализовать.
Код проекта открыт, велкам в пул-реквесты. Адрес сайта — в конце статьи.
Архитектура
После «долгих и тщательных раздумий» основой бота была выбрана библиотека Nutgram: наиболее лёгкая, простая и современная. Бонусом идёт полностью настроенный DI-контейнер, благодараю которому можно забыть о ручной инициализации сервисов и их передаче потребителям.
А использование PHP 8.1 позволило написать чуть меньше кода и получить чуть выше производительность. Promoted properties, readonly и строгая типизация сильно облегчают разработку.
Настройки composer’а максимально облегчены аналогично API. Итоговый composer.json.
Обновления от Telegram приходят на webhook endpoint и раздаются обработчикам определённых команд и типов сообщений. Обработчики отвечают самостоятельно или обращаются к REST API за данными. Дополнительно есть Fallback, Exception и ApiError-обработчики для всяких неожиданностей.
Использование коротких single-action invokable-обработчиков позволило уместить всю логику бота в 23 строки!

Пример команды /nearest:
<?php declare(strict_types=1); namespace App\Commands; use App\Contracts\Command; use SergiX44\Nutgram\Nutgram; use SergiX44\Nutgram\Telegram\Types\Keyboard\KeyboardButton; use SergiX44\Nutgram\Telegram\Types\Keyboard\ReplyKeyboardMarkup; use SergiX44\Nutgram\Telegram\Types\Message\Message; final class NearestCommand implements Command { public const SEND_TEXT = 'Send location'; public function __invoke(Nutgram $bot): ?Message { return $bot->sendMessage('Send your location to find the nearest coffee shop', [ 'reply_markup' => ReplyKeyboardMarkup::make(resize_keyboard: true)->addRow(KeyboardButton::make(self::SEND_TEXT, request_location: true)), ]); } public static function getName(): string { return 'nearest'; } public static function getDescription(): string { return 'Show nearest specialty coffee shop'; } }
Пример обработчика местоположения:
<?php declare(strict_types=1); namespace App\Handlers; use App\Services\ApiService; use App\Services\Sender; use SergiX44\Nutgram\Nutgram; use SergiX44\Nutgram\Telegram\Types\Message\Message; final class LocationHandler { public function __construct(private readonly ApiService $api, private readonly Sender $sender) { } public function __invoke(Nutgram $bot): ?Message { $location = $bot->message()->location; return $this->sender->sendItem( $this->api->getNearest((string)$location->latitude, (string)$location->longitude), [ 'reply_markup' => ['remove_keyboard' => true], ] ); } }
Для проверки легитимности сообщений, а также для валидации даных для поиска, используются такие же короткие и ёмкие middleware.
Конфигурация
Общие параметры и названия секретов — в .env, локальные переопределения — в .env.local
Тесты
Пока руками ¯_(ツ)_/¯
Но в библиотеке Nutgram есть достаточные возможно написания тестов, это радует.
Мониторинг
Sentry, в .env достаточно указать пустое значение SENTRY_DSN (для наглядности), а фактическое значение записать в секрет.
Деплой
Всё та же платформа Fly.io, но теперь с machines, загружающимися за 300ms. В общем случае это FaaS (serverless), но в моём случае с php-сервером — это всё-таки обычная VM.
Ради интереса использую встроенный php-сервер вместо обычного сочетания php-fpm + nginx/caddy + supervisor. Docker-образ, конечно, стал меньше, но пришлось использовать отдельный роутер:
-
Для пропуска только POST-запросов к обработчикам бота
-
Для редиректа dev-домена вида .fly.dev на основной домен
-
Раздачи статики (robots.txt, favicon.ico и т.д.)
-
Блокировки всех остальных запросов
Итоговый роутер и Dockerfile (такой же слоёный как в API).
CI/CD
Github Action достаточно прост: получаем ID запущенной машины из flyctl machine list --json, обновляем её flyctl machine run --id <id> и обновляем регистрацию вебхука curl -sS ${{ secrets.APP_URL }}/setup.php.
Все секреты хранятся на платформе хостинга и частично дублируются в GitHub production Environment для регистрации вебхука.
На этом этапе бот работает, размещён в продакшн-окружении и доступен всем пользователям. Проект полностью выполнен 🙂
Репозиторий бота, сайт https://specialtycoffee.cy/
TODO
Теперь, после запуска проекта в спокойном порядке, можно:
-
настроить внешний мониторинг доступности
-
health check’и по настоящему ответу сервисов, а не просто «живучести» порта
-
оптимизировать сборку с Caddy
-
попробовать Buildpack
-
заменить встроенный PHP-сервер бота на что-то более безопасное
-
добавить типизацию (Typescript)
-
добавить статистику использования API
-
добавить статистику использования бота
-
расширить отслеживание ссылок и событий в Google Analytics
-
заменить Google Analytics на что-то полегче и более соответствующее GDPR.
Велкам в комментарии!
ссылка на оригинал статьи https://habr.com/ru/post/677594/
Добавить комментарий