From 926b77a4290f6c773fb7e785d134f44625e9375b Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 4 Jan 2023 23:09:58 +0800 Subject: [PATCH] add prompt for bot context --- src/ZM/Context/BotContext.php | 55 ++++++++++++++++++++ src/ZM/Plugin/OneBot12Adapter.php | 84 ++++++++++++++++++++++++++++--- 2 files changed, 131 insertions(+), 8 deletions(-) diff --git a/src/ZM/Context/BotContext.php b/src/ZM/Context/BotContext.php index faa0313c..414d8f4f 100644 --- a/src/ZM/Context/BotContext.php +++ b/src/ZM/Context/BotContext.php @@ -6,12 +6,15 @@ namespace ZM\Context; use DI\DependencyException; use DI\NotFoundException; +use OneBot\Driver\Coroutine\Adaptive; use OneBot\Driver\Event\Http\HttpRequestEvent; use OneBot\Driver\Event\WebSocket\WebSocketMessageEvent; use OneBot\V12\Object\MessageSegment; use OneBot\V12\Object\OneBotEvent; use ZM\Context\Trait\BotActionTrait; use ZM\Exception\OneBot12Exception; +use ZM\Exception\WaitTimeoutException; +use ZM\Plugin\OneBot12Adapter; use ZM\Utils\MessageUtil; class BotContext implements ContextInterface @@ -74,6 +77,58 @@ class BotContext implements ContextInterface throw new OneBot12Exception('bot()->reply() can only be used in message event.'); } + /** + * 在当前会话等待用户一条消息 + * 如果是私聊,就在对应的机器人私聊环境下等待 + * 如果是单级群组,就在对应的群组下等待当前消息人的消息 + * 如果是多级群组,则等待最小级下当前消息人的消息 + * + * @param array|MessageSegment|string|\Stringable $prompt 等待前发送的消息文本 + * @param int $timeout 等待超时时间(单位为秒,默认为 600 秒) + * @param string $timeout_prompt 超时后提示的消息内容 + * @param bool $return_string 是否只返回 text 格式的字符串消息(默认为 false) + * @throws DependencyException + * @throws NotFoundException + * @throws OneBot12Exception + * @throws WaitTimeoutException + */ + public function prompt(string|\Stringable|MessageSegment|array $prompt = '', int $timeout = 600, string $timeout_prompt = '', bool $return_string = false): array|string + { + if (!container()->has('bot.event')) { + throw new OneBot12Exception('bot()->prompt() can only be used in message event'); + } + /** @var OneBotEvent $event */ + $event = container()->get('bot.event'); + if ($event->type !== 'message') { + throw new OneBot12Exception('bot()->prompt() can only be used in message event'); + } + // 开始等待输入 + logger()->debug('Waiting user for prompt...'); + if ($prompt !== '') { + $this->reply($prompt); + } + if (($co = Adaptive::getCoroutine()) === null) { + throw new OneBot12Exception('Coroutine is not supported yet, prompt() not works'); + } + $cid = $co->getCid(); + OneBot12Adapter::addContextPrompt($cid, $event); + $co->create(function () use ($cid, $timeout) { + Adaptive::sleep($timeout); + if (OneBot12Adapter::isContextPromptExists($cid)) { + Adaptive::getCoroutine()->resume($cid, ''); + } + }); + $result = $co->suspend(); + OneBot12Adapter::removeContextPrompt($cid); + if ($result === '') { + throw new WaitTimeoutException($this, $timeout_prompt); + } + if ($result instanceof OneBotEvent && $result->type === 'message') { + return $return_string ? $result->getMessageString() : $result->getMessage(); + } + throw new OneBot12Exception('Internal error for resuming prompt: unknown type ' . gettype($result)); + } + /** * 返回是否已经调用过回复了 */ diff --git a/src/ZM/Plugin/OneBot12Adapter.php b/src/ZM/Plugin/OneBot12Adapter.php index d353d55e..43b8485e 100644 --- a/src/ZM/Plugin/OneBot12Adapter.php +++ b/src/ZM/Plugin/OneBot12Adapter.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace ZM\Plugin; use Choir\Http\HttpFactory; +use OneBot\Driver\Coroutine\Adaptive; use OneBot\Driver\Event\StopException; use OneBot\Driver\Event\WebSocket\WebSocketMessageEvent; use OneBot\Driver\Event\WebSocket\WebSocketOpenEvent; @@ -34,13 +35,19 @@ class OneBot12Adapter extends ZMPlugin { /** * 缓存待询问参数的队列 - * * 0: 代表 OneBotEvent 对象,即用于判断是否为同一会话环境 * 1: \Generator 生成器,协程,不多讲 * 2: BotCommand 注解对象 * 3: match_result(array)匹配到一半的结果 + * + * @var array 队列 */ - private static array $prompt_queue = []; + private static array $argument_prompt_queue = []; + + /** + * @var array 队列 + */ + private static array $context_prompt_queue = []; public function __construct(string $submodule = '', ?AnnotationParser $parser = null) { @@ -54,7 +61,8 @@ class OneBot12Adapter extends ZMPlugin // 在 BotEvent 内处理 BotCommand $this->addBotEvent(BotEvent::make(type: 'message', level: 15)->on([$this, 'handleBotCommand'])); // 在 BotEvent 内处理需要等待回复的 CommandArgument - $this->addBotEvent(BotEvent::make(type: 'message', level: 20)->on([$this, 'handlePrompt'])); + $this->addBotEvent(BotEvent::make(type: 'message', level: 49)->on([$this, 'handleCommandArgument'])); + $this->addBotEvent(BotEvent::make(type: 'message', level: 50)->on([$this, 'handleContextPrompt'])); // 处理和声明所有 BotCommand 下的 CommandArgument $parser->addSpecialParser(BotCommand::class, [$this, 'parseBotCommand']); // 不需要给列表写入 CommandArgument @@ -67,6 +75,34 @@ class OneBot12Adapter extends ZMPlugin } } + /** + * @internal 只允许内部使用 + * @param int $cid 协程 ID + * @param OneBotEvent $event 事件对象 + */ + public static function addContextPrompt(int $cid, OneBotEvent $event): void + { + self::$context_prompt_queue[$cid] = $event; + } + + /** + * @internal 只允许内部使用 + * @param int $cid 协程 ID + */ + public static function removeContextPrompt(int $cid): void + { + unset(self::$context_prompt_queue[$cid]); + } + + /** + * @internal 只允许内部使用 + * @param int $cid 协程 ID + */ + public static function isContextPromptExists(int $cid): bool + { + return isset(self::$context_prompt_queue[$cid]); + } + /** * 将 BotCommand 假设含有 CommandArgument 的话,就注册到参数列表中 * @@ -122,7 +158,7 @@ class OneBot12Adapter extends ZMPlugin $message = MessageSegment::text($argument->prompt === '' ? ('请输入' . $argument->name) : $argument->prompt); $ctx->reply([$message]); // 然后将此事件放入等待队列 - self::$prompt_queue[] = [$ctx->getEvent(), $arguments, $command, $match_result]; + self::$argument_prompt_queue[] = [$ctx->getEvent(), $arguments, $command, $match_result]; return; } $ctx->setParams($arguments); @@ -139,12 +175,12 @@ class OneBot12Adapter extends ZMPlugin * @throws OneBot12Exception * @throws \Throwable */ - public function handlePrompt(BotContext $ctx) + public function handleCommandArgument(BotContext $ctx) { // 需要先从队列里找到定义当前会话的 prompt // 定义一个会话的标准是:事件的 detail_type,user_id,[group_id],[guild_id,channel_id] 全部相同 $new_event = $ctx->getEvent(); - foreach (self::$prompt_queue as $k => $v) { + foreach (self::$argument_prompt_queue as $k => $v) { /** @var OneBotEvent $old_event */ $old_event = $v[0]; if ($old_event->detail_type !== $new_event->detail_type) { @@ -159,7 +195,7 @@ class OneBot12Adapter extends ZMPlugin if (!$matched) { continue; } - array_splice(self::$prompt_queue, $k, 1); + array_splice(self::$argument_prompt_queue, $k, 1); // 找到了,开始处理 /** @var \Generator $arguments */ $arguments = $v[1]; @@ -171,7 +207,7 @@ class OneBot12Adapter extends ZMPlugin $message = MessageSegment::text($argument->prompt === '' ? ('请输入' . $argument->name) : $argument->prompt); $ctx->reply([$message]); // 然后将此事件放入等待队列 - self::$prompt_queue[] = [$ctx->getEvent(), $arguments, $v[2], $v[3]]; + self::$argument_prompt_queue[] = [$ctx->getEvent(), $arguments, $v[2], $v[3]]; } else { // 所有参数都已经获取到了,调用方法 $ctx->setParams($arguments->getReturn()); @@ -181,6 +217,38 @@ class OneBot12Adapter extends ZMPlugin } } + /** + * [CALLBACK] 处理需要等待回复的 bot()->prompt() 会话消息 + * + * @param BotContext $ctx 机器人上下文 + * @param OneBotEvent $event 当前事件对象 + * @throws InterruptException + */ + public function handleContextPrompt(BotContext $ctx, OneBotEvent $event) + { + // 必须支持协程才能用 + if (($co = Adaptive::getCoroutine()) === null) { + return; + } + // 遍历等待的信息会话列表 + foreach (self::$context_prompt_queue as $cid => $v) { + // 类型得一样 + if ($v->detail_type !== $event->detail_type) { + continue; + } + $matched = match ($v->detail_type) { + 'private' => $v->getUserId() === $event->getUserId(), + 'group' => $v->getGroupId() === $event->getGroupId() && $v->getUserId() === $event->getUserId(), + 'channel' => $v->getGuildId() === $event->getGuildId() && $v->getChannelId() === $event->getChannelId() && $v->getUserId() === $event->getUserId(), + default => false, + }; + if ($matched) { + $co->resume($cid, $event); + AnnotationHandler::interrupt(); + } + } + } + /** * @throws StopException */