务实开发3:电报机器人
#教程 #php #telegram #聊天机器人

关于塞浦路斯专业咖啡店的简单项目的结论。 first part专注于API微服务,前端站点上的second,第三个在Telegram Bot上。

机器人从简单的任务开始:

  • /地图 - 显示咖啡店的地图
  • /list - 显示咖啡店清单
  • 展示咖啡店详细信息
  • /Random - 显示随机咖啡店
  • 搜索名称
  • 的咖啡店
  • 按位置或/最近的命令搜索最近的咖啡店

在实施过程中,发现电报无法显示带有多个标记的嵌入式地图,也不会在Web版本中发送位置。结果,我不得不将带有地图和存根消息的网站显示指向网站的链接,而不是在空位置上的响应。其他一切都已经完成。

project code开放,bot https://t.me/SpecialtyCoffeeCyBot

建筑学

经过大量审议,将Nutgram选为机器人的基础:它是最轻巧,简单和现代的图书馆。配置完整的DI容器是一种奖励,使您可以避免手动服务初始化和向客户交付。

使用PHP 8.1的实际版本使我编写较少的代码,同时实现性能稍好。通过促进属性,仅阅读属性和严格的键入使开发变得容易得多。

作曲家的设置很简单,如API。最终的composer.json文件。

电报更新是在Webhook端点接收的,并将其传输到命令和消息类型的处理程序。处理程序可以自行响应或请求REST API的数据。对于意外的,有后备,异常和Apierror处理程序。

使用简短的单次动力传输者允许bot's logic仅凝结成32行!

$bot = new Nutgram($_ENV['BOT_TOKEN'], [
    'timeout' => $_ENV['CONNECT_TIMEOUT'],
    'logger' => ConsoleLogger::class
]);
$bot->setRunningMode(Webhook::class);

$bot->middleware(AuthMiddleware::class);

$bot->fallback(FallbackHandler::class);
$bot->onException(ExceptionHandler::class);
$bot->onApiError(ApiErrorHandler::class);

$bot->onText(NearestCommand::SEND_TEXT, NotSupportedHandler::class);

$bot->onMessageType(MessageTypes::TEXT, SearchHandler::class)->middleware(SearchRequirementsMiddleware::class);
$bot->onMessageType(MessageTypes::LOCATION, LocationHandler::class);

$bot->onMessageType(MessageTypes::NEW_CHAT_MEMBERS, NullHandler::class);
$bot->onMessageType(MessageTypes::LEFT_CHAT_MEMBER, NullHandler::class);

$bot->onCommand(ListCommand::getName(), ListCommand::class)->description(ListCommand::getDescription());
$bot->onCommand(MapCommand::getName(), MapCommand::class)->description(MapCommand::getDescription());
$bot->onCommand(NearestCommand::getName(), NearestCommand::class)->description(NearestCommand::getDescription());
$bot->onCommand(RandomCommand::getName(), RandomCommand::class)->description(RandomCommand::getDescription());
$bot->onCommand(StartCommand::getName(), StartCommand::class)->description(StartCommand::getDescription());

$bot->registerMyCommands();

$http = new Client(['base_uri' => $_ENV['API_URL']]);
$bot->getContainer()->addShared(Client::class, $http);

$bot->run();

/nearest command9示例:

final class NearestCommand extends BaseCommand
{
    public const SEND_TEXT = 'Send location';


    public static function getName(): string
    {
        return 'nearest';
    }


    public static function getDescription(): string
    {
        return 'Show nearest specialty coffee shop';
    }


    public function getAnswer(): Answer
    {
        return new TextAnswer('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)),
        ]);
    }
}

Location handler示例:

final class LocationHandler extends BaseHandler
{
    private Location $location;


    public function __construct(Sender $sender, private readonly ApiService $api)
    {
        parent::__construct($sender);
    }


    public function __invoke(Nutgram $bot): ?Message
    {
        $this->location = $bot->message()->location;

        return $this->sender->send($this->getAnswer());
    }


    /** @inheritDoc */
    public function getAnswer(): Answer|array
    {
        $cafe = $this->api->getNearest((string)$this->location->latitude, (string)$this->location->longitude);

        return [
            new TextAnswer(Formatter::item($cafe), ['parse_mode' => ParseMode::HTML]),

            new VenueAnswer((float)$cafe->latitude, (float)$cafe->longitude, $cafe->name, '', [
                'google_place_id' => $cafe->placeId,
                'reply_to_message_id' => 0,
                'reply_markup' => ['remove_keyboard' => true],
            ]),
        ];
    }


    public function getLocation(): Location
    {
        return $this->location;
    }


    public function setLocation(Location $location): LocationHandler
    {
        $this->location = $location;

        return $this;
    }
}

使用相同的简短而简洁的middleware来验证搜索数据以及检查消息的合法性。

配置

常见选项和秘密名称存储在.env文件中,而本地覆盖物存储在.env.local文件中。

测试

我很高兴Nutgram库为编写测试提供了足够的可能性。与简单的guzle模拟不同,项目的tests使用模拟的ApiClient,它返回预定义的响应无限的次数。

监视

与API微服务和前端中的Sentry相同的Sentry,在.env中,仅指定了Sentry_dsn的空值(为了清楚起见),然后将实际值写入秘密。

部署

仍然是同一Fly.io平台,但现在具有300ms载的时间Machines。通常,它是FAA(无服务器),但就我而言,使用PHP服务器,它仍然是常规VM。

我使用了嵌入式PHP服务器,而不是PHP-FPM + NGINX/CADDY +主管的通常组合来加快项目的启动。 Docker图像当然变小,但是我不得不使用一个单独的路由器:

  • 仅通过发布请求到bot处理程序
  • 将Dev域重新定向。
  • 分发静态(robots.txt,favicon.ico等)
  • 阻止所有其他请求

final routerDockerfile(与API部分相同)。

CI/CD

Github Action足够简单:更新机器flyctl deploy并更新Webhook注册curl -sS ${{ secrets.APP_URL }}/setup.php

所有秘密都存储在托管平台上,并在github生产环境中部分复制了Webhook注册。

在此阶段,该机器人已直播,托管在制作中,并向所有用户使用。该项目已完全完成: - )

Bot repository,网站https://specialtycoffee.cy/

全部

整个项目的最终非关键任务:

  • 健康检查是否有真正的服务响应,而不仅仅是端口“生存能力”。
  • 用球童优化构建
  • 尝试buildpack或nixpack
  • 用更安全的东西替换内置的PHP服务器
  • 添加严格的打字(Typescript)
  • 添加API用法统计
  • 添加机器人用法统计
  • 扩展Google Analytics(分析)中的链接和事件跟踪
  • 用更轻,更符合GDPR的东西替换Google Analytics(