botasis/runtime

Bot运行时库,用于处理接收到的更新


README

Botasis Runtime是一个强大且灵活的PHP库,旨在简化Telegram机器人应用程序的开发。它作为构建Telegram机器人的基础框架,提供必要的抽象和功能,使得创建交互式和智能聊天机器人变得前所未有的容易。

示例

以下是一个简单的Telegram机器人示例,它将根据您的请求发送货币汇率,但前提是您已经为此功能支付了费用。

  1. 创建路由
    [
      (new Group(
        // This bot will work in private chats only
        new RuleDynamic(static fn(Update $update) => $update->chat->type === ChatType::PRIVATE),
        ...[
          '/start' => new Route(
            new RuleStatic('/start'),
            [StartAction::class, 'handle'],
          ),
          '/pay' => new Route(
            new RuleStatic('/pay'),
            [PayAction::class, 'handle'],
          ),
          'rates requests' => (new Route(
            new RuleDynamic(
              static fn(Update $update): bool => preg_match('#^/rates \w+ \w+$#', $update->requestData ?? '') === 1,
            ),
            [RatesAction::class, 'handle'],
          ))->withMiddlewares(PaidAccessMiddleware::class),
        ],
      ))->withMiddlewares(UserRegisterMiddleware::class),
    ]
    我们在这里将三个命令映射到相应的操作。/start/pay 命令是静态路由。这意味着只有当它们与用户发送的完全匹配时,才会被映射。
    另一方面,/rates 命令是动态的,应该看起来像 /rates USD GBP
  2. 创建命令处理程序。
    1. 最简单的一个是 StartAction。它只会发送一条问候消息。
      final readonly class StartAction
      {
        public function handle(Update $update): ResponseInterface
        {
          return (new Response($update))
            ->withRequest(
              new Message(
                // You should properly escape all special characters to send a markdown message
                'Hello\\! Use \\/pay command to get premium\\, and then use \\/rates command ' .
                    'to see currency exchange rates\\. I\\.e\\. `/pay USD GBP`\\.',
                MessageFormat::MARKDOWN, 
                $update->chat->id,
              ),
            );
        }
      }
    2. 接受支付超出了本例的范围。我将跳过 PayAction 处理程序,因为它与 RatesAction 相比没有有趣的内容。
    3. RatesAction 处理程序
      final readonly class RatesAction
      {
          public function __construct(private readonly RatesService $ratesService) {}
      
          public function handle(
              Update $update,
              #[UpdateAttribute('user')] User $user,
          ): ResponseInterface {
            // User sent a request like "/rates USD GBP", so we need to get USD and GBP from the request
            [, $currency1, $currency2] = explode(' ', $update->requestData);
            
            $user->saveRequestHistory($currency1, $currency2);
            $rate = $this->ratesService->getRate($currency1, $currency2);
            $date = date('Y-m-d H:i:s');
            
            // send a message as a response
            return (new Response($update))
              ->withRequest(
                new Message(
                  "1 $currency1 = $rate $currency2 on $date. To get a reverse rate, " .
                      "use /rates $currency2 $currency1 command.",
                  MessageFormat::TEXT, 
                  $update->chat->id,
                ),
              );
          }
      }
  3. 创建中间件。
    1. UserRegisterMiddleware 被分配到外部组。因此,它总是在每个处理程序之前执行。它注册一个用户并将其添加到Update对象中作为属性
      final class UserRegisterMiddleware implements MiddlewareInterface
      {
          public function __construct(private UserRepository $repository)
          {
          }
      
          public function process(Update $update, UpdateHandlerInterface $handler): ResponseInterface
          {
              // Repository either finds a user or creates a new one
              $holder = $this->repository->getUser($update->user, $update->chat->id);
      
              // now $update->getAttribute('user') contains a User object 
              return $handler->handle($update->withAttribute('user', $holder));
          }
      }
      由于这个中间件,请求处理程序可以使用 #[UpdateAttribute('user')] 属性并获取一个类型化的 User 对象(请参阅 RatesAction::handle())。
    2. PaidAccessMiddleware 不允许非高级用户进行汇率请求。它也准备好附加到任何其他付费端点。
      final class PaidAccessMiddleware implements MiddlewareInterface
      {
          public function process(Update $update, UpdateHandlerInterface $handler): ResponseInterface
          {
              /** @var User $user */
              $user = $update->getAttribute('user');
              if (!$user->isPremium()) {
                  return (new Response($update))
                      ->withRequest(new Message(
                          'Only premium users can use this command',
                          MessageFormat::TEXT,
                          $update->chat->id,
                      ));
              }
      
              return $handler->handle($update);
          }
      }

使用Botasis Runtime,您不必担心Telegram基础设施。只需像处理HTTP请求一样编写您的业务逻辑即可!

主要功能

  • 中间件堆栈:Botasis Runtime提供了一个强大的中间件系统,允许您轻松定义和组织传入的Telegram更新的处理。通过中间件,您可以实现各种行为和逻辑,例如身份验证、消息预处理和后处理等。

  • 路由和分发:该库包括一个灵活且高效的路由系统,使您能够为特定的Telegram命令或交互定义路由。路由组、内部中间件和路由参数捕获提供了对更新处理的精细控制。

  • 更新处理:Botasis Runtime简化了Telegram更新的管理和操作。它提供了一个直观的API来访问和修改更新数据,使与用户交互和响应用户消息变得轻松。

  • 框架无关性:Botasis Runtime设计为框架无关,这意味着它可以无缝集成到各种PHP应用程序和框架中。无论您是否使用特定的PHP框架或开发独立的机器人应用程序,Botasis Runtime都能适应您的项目需求。

  • 可扩展性:通过添加您自己的中间件、操作和自定义逻辑来扩展和自定义您的Telegram机器人的行为。它具有您可能需要的所有扩展点。

  • 配置:微调机器人设置、路由规则和中间件堆栈,创建一个完全符合您愿景的机器人。无需硬编码,一切均可配置。

  • 可伸缩性:随着您的机器人成长和演变,Botasis Runtime使您能够轻松地进行更改和改进,确保您的机器人能够适应新的功能和用户交互。

  • 支持最佳实践

    • PSR启用。没有硬编码的依赖项。您可以使用任何PSR实现。
    • SOLID代码。您一定会喜欢依赖倒置原则的实现,因为它允许您将任何东西传递到任何地方,并且可以轻松地切换实现。
    • 支持长运行。它已准备好与RoadRunnerSwoole和其他长运行应用程序引擎一起使用。这意味着Botasis运行时没有任何有状态的服务,并且永远不会重复计算同一件事

快速入门

使用Botasis开始您的应用程序最快的方法是使用Botasis应用程序模板

如果您不想使用它,或者您想将Botasis嵌入到现有的应用程序中,请按照以下步骤操作

  1. 使用Composer安装Botasis运行时和所有依赖项

    composer require botasis/runtime httpsoft/http-message php-http/socket-client yiisoft/event-dispatcher yiisoft/di
    包详细信息
    • botasis/runtime - 此包,必需
    • httpsoft/http-message - PSR-7 (HTTP Message) 和 PSR-17 (HTTP Factories) 的实现。您可以使用任何实现,但个人我更喜欢这个。
    • php-http/socket-client - PSR-18 (HTTP Client) 的实现。您可以使用任何实现。
    • yiisoft/event-dispatcher - PSR-14 (Event Dispatcher) 的实现。您可以使用任何实现,但个人我更喜欢这个,因为它是很好的且与框架无关的实现。
    • yiisoft/di - PSR-11 (DI Container) 的实现。您可以使用任何实现,但个人我更喜欢这个,因为它是非常高效、方便且与框架无关的实现。
  2. 创建一个新的PHP脚本来初始化您的机器人。通常DI容器会处理大部分这些工作。

    PHP脚本列表
    use Botasis\Client\Telegram\Client\ClientPsr;
    use Botasis\Runtime\Application;
    use Botasis\Runtime\CallableFactory;
    use Botasis\Runtime\Emitter;
    use Botasis\Runtime\Handler\DummyUpdateHandler;
    use Botasis\Runtime\Middleware\Implementation\RouterMiddleware;
    use Botasis\Runtime\Middleware\MiddlewareDispatcher;
    use Botasis\Runtime\Middleware\MiddlewareFactory;
    use Botasis\Runtime\Router\Route;
    use Botasis\Runtime\Router\Router;
    use Botasis\Runtime\Router\RuleStatic;
    use Botasis\Runtime\UpdateHandlerInterface;
    use Http\Client\Socket\Client;
    use HttpSoft\Message\RequestFactory;
    use HttpSoft\Message\StreamFactory;
    use Psr\Container\ContainerInterface;
    use Psr\EventDispatcher\EventDispatcherInterface;
    use Psr\Http\Client\ClientInterface;
    use Psr\Http\Message\RequestFactoryInterface;
    use Psr\Http\Message\StreamFactoryInterface;
    use Yiisoft\Di\Container;
    use Yiisoft\EventDispatcher\Dispatcher\Dispatcher;
    
    /**
    * @var string $token - a bot token you've got from the BotFather
    * @var ClientInterface $httpClient - an HTTP client. If you've installed the php-http/socket-client package,
    *                                    it's {@see Client}. Either it's a client of your choice.
    * @var RequestFactoryInterface $requestFactory - a PSR-17 HTTP request factory. If you've installed the httpsoft/http-message package,
    *                                                it's {@see RequestFactory}.
    * @var StreamFactoryInterface $streamFactory - a PSR-17 HTTP stream factory. If you've installed the httpsoft/http-message package,
    *                                              it's {@see StreamFactory}.
    * @var EventDispatcherInterface $eventDispatcher - a PSR-14 event dispatcher. If you've installed the yiisoft/event-dispatcher package,
    *                                                  it's {@see Dispatcher}.
    * @var ContainerInterface $container - a PST-11 DI container. If you've installed the yiisoft/di package,
    *                                      it's {@see Container}.
    */
    
    $client = new ClientPsr(
      $token,
      $httpClient,
      $requestFactory,
      $streamFactory,
    );
    $emitter = new Emitter($client, $eventDispatcher);
    
    $middlewareDispatcher = new MiddlewareDispatcher(
      new MiddlewareFactory($container, new CallableFactory($container)),
      $eventDispatcher,
    );
    
    /**
    * Routes definition. Here we define a route for the /start message. The HelloAction will be instantiated by a DI container.
    */
    $routes = [
        new Route(new RuleStatic('/start'), [HelloAction::class, 'handle']),
    ];
    
    /**
    * Middlewares definition. {@see RouterMiddleware} should be the last one.
    */
    $middlewares = [new RouterMiddleware(new Router($container, $middlewareDispatcher, ...$routes))];
    
    $middlewareDispatcher = $middlewareDispatcher->withMiddlewares();
    $application = new Application($emitter, new DummyUpdateHandler(), $middlewareDispatcher);
  3. 根据您的机器人的行为和需求,通过注册中间件、操作和路由来自定义您的机器人。

  4. 开始接收更新。您可以使用GetUpdatesCommand从Telegram API拉取更新(在本地工作时)或使用SetTelegramWebhookCommand设置您的机器人的webhook地址,这样Telegram会自己向您发送更新。

就这样!您现在已使用Botasis运行时为您的机器人设置了基础。您可以通过自定义操作、中间件和路由来增强机器人的功能,为您的用户提供引人入胜的交互式体验。

功能

1. 路由

您可以为您的高级机器人创建路由和子路由。每个路由由两部分组成;

  • A Rule。当来自Telegram的Update到来时,Router检查它是否满足每个路由的规则。一旦找到这样的路由,其Action就会被执行。有两种类型的规则
    • RuleStatic。它映射到Update中的消息或回调数据。当从Telegram收到消息或回调时,它与每个现有的RuleStatic进行比较。创建此类规则非常简单:new RuleStatic('/command')。只有在没有合适的静态规则时,我们才会进一步到RuleDynamic列表。

    • RuleDynamic。与RuleStatic相反,这种规则类型对每个Update执行可调用。此类规则的创建可能如下所示:new RuleDynamic(static fn(Update $update) => str_starts_with($update->requestData ?? '', '/start@myBot'))
      此类可调用必须返回一个布尔值。可调用定义应遵循扩展可调用定义格式。

      详细定义路由.

  • 一个 操作。它是一个可调用的,可以像 RuleDynamic 可调用一样定义。但是路由操作的返回值 必须null/voidResponseInterface。在其他所有情况下,路由器将抛出异常。

1. 路由中属性的用法

1. 状态管理

当您的应用程序需要处理聊天或用户状态时,此功能至关重要。按照以下四个步骤使用它

  1. 实现 StateRepositoryInterface
    您可以使用任何现有的实现 (它们将在以后实现)
  2. 使用存储库保存用户/聊天状态
    final class CharacterNameCommandAction
    {
    public function __construct(private StateRepositoryInterface $repository) {}
    
        public function handle(Update $update): ResponseInterface
        {
            $state = new StateJson($update->user->id, $update->chat->id, 'setting-name');
            $this->repository->save($state);
    
            return (new Response($update))
                ->withRequest(new Message(
                    'Enter your character name below',
                    MessageFormat::TEXT,
                    $update->chat->id,
                ));
        }
    }
  3. 在路由器中间件之前添加 StateMiddleware
    这允许您在路由操作中访问当前状态。
    $state = $update->getAttribute(\Botasis\Runtime\State\StateMiddleware::class);

    注意! 此中间件搜索用户和聊天 ID。如果您只需要用户或仅需要聊天 ID,您需要自己实现此逻辑。

  4. 在路由中使用状态
    [
        new Route(new RuleStatic('/set_name'), CharacterNameCommandAction::class),
        new Route(
            new RuleDynamic(static fn(Update $update) => $update->getAttributes(StateMiddleware::class)?->getData() === json_encode('setting-name')),
            CharacterNameSetAction::class,
        ),
    ]

如果您更喜欢创建严格类型的 State 对象,则需要实现 StateInterface 以及 StateRepositoryInterface