Прагматичная разработка-3: телеграм-бот

от автора

Спешлти-кофеен на Кипре стало ещё больше
Спешлти-кофеен на Кипре стало ещё больше

Финальная часть разработки простого проекта про specialty-кофейни на Кипре. В первой части я рассказал про API микросервис, во второй — про фронтэнд-сайт и теперь — про телеграм-бота.

Изначально перед ботом ставились простые задачи:

  1. /map — карта кофеен

  2. /list — список кофеен

  3. подробности о кофейне

  4. /random — случайная кофейня

  5. поиск кофейни по названию

  6. поиск ближайшей кофейни по своему местоположению или по команде /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-образ, конечно, стал меньше, но пришлось использовать отдельный роутер:

  1. Для пропуска только POST-запросов к обработчикам бота

  2. Для редиректа dev-домена вида .fly.dev на основной домен

  3. Раздачи статики (robots.txt, favicon.ico и т.д.)

  4. Блокировки всех остальных запросов

Итоговый роутер и 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *