zhamao-framework/src/ZM/Plugin/OneBot12Adapter.php
2023-02-10 14:03:48 +08:00

713 lines
31 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
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;
use OneBot\V12\Exception\OneBotException;
use OneBot\V12\Object\ActionResponse;
use OneBot\V12\Object\MessageSegment;
use OneBot\V12\Object\OneBotEvent;
use OneBot\V12\Validator;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use ZM\Annotation\AnnotationHandler;
use ZM\Annotation\AnnotationMap;
use ZM\Annotation\AnnotationParser;
use ZM\Annotation\OneBot\BotActionResponse;
use ZM\Annotation\OneBot\BotCommand;
use ZM\Annotation\OneBot\BotEvent;
use ZM\Annotation\OneBot\CommandArgument;
use ZM\Container\ContainerRegistrant;
use ZM\Context\BotContext;
use ZM\Exception\InterruptException;
use ZM\Exception\OneBot12Exception;
use ZM\Exception\WaitTimeoutException;
use ZM\Utils\ConnectionUtil;
use ZM\Utils\MessageUtil;
class OneBot12Adapter extends ZMPlugin
{
/**
* 缓存待询问参数的队列
* 0: 代表 OneBotEvent 对象,即用于判断是否为同一会话环境
* 1: \Generator 生成器,协程,不多讲
* 2: BotCommand 注解对象
* 3: match_resultarray匹配到一半的结果
*
* @var array<int, array> 队列
*/
private static array $argument_prompt_queue = [];
/**
* @var array<int, OneBotEvent> 队列
*/
private static array $context_prompt_queue = [];
public function __construct(string $submodule = '', ?AnnotationParser $parser = null)
{
switch ($submodule) {
case '':
case 'onebot12':
// 处理所有 OneBot 12 的反向 WS 握手事件
$this->addEvent(WebSocketOpenEvent::class, [$this, 'handleWSReverseOpen']);
$this->addEvent(WebSocketMessageEvent::class, [$this, 'handleWSReverseMessage']);
// 在 BotEvent 内处理 BotCommand
$this->addBotEvent(BotEvent::make(type: 'message', level: 15)->on([$this, 'handleBotCommand']));
// 在 BotEvent 内处理需要等待回复的 CommandArgument
$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
$parser->addSpecialParser(CommandArgument::class, fn () => true);
break;
case 'onebot12-ban-other-ws':
// 禁止其他类型的 WebSocket 客户端接入
$this->addEvent(WebSocketOpenEvent::class, [$this, 'handleUnknownWSReverseInput'], 1);
break;
}
}
/**
* @param int $cid 协程 ID
* @param OneBotEvent $event 事件对象
* @internal 只允许内部使用
*/
public static function addContextPrompt(int $cid, OneBotEvent $event): void
{
self::$context_prompt_queue[$cid] = $event;
}
/**
* @param int $cid 协程 ID
* @internal 只允许内部使用
*/
public static function removeContextPrompt(int $cid): void
{
unset(self::$context_prompt_queue[$cid]);
}
/**
* @param int $cid 协程 ID
* @internal 只允许内部使用
*/
public static function isContextPromptExists(int $cid): bool
{
return isset(self::$context_prompt_queue[$cid]);
}
/**
* 将 BotCommand 假设含有 CommandArgument 的话,就注册到参数列表中
*
* @param BotCommand $command 命令对象
* @param null|array $same_method_annotations 同一个方法的所有注解
*/
public function parseBotCommand(BotCommand $command, ?array $same_method_annotations = null): ?bool
{
if ($same_method_annotations === null) {
return null;
}
foreach ($same_method_annotations as $v) {
if ($v instanceof CommandArgument) {
$command->withArgumentObject($v);
}
}
return null;
}
/**
* [CALLBACK] 调用 BotCommand 注解的方法
*
* @param BotContext $ctx 机器人环境上下文
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws \Throwable
* @throws OneBot12Exception
*/
public function handleBotCommand(BotContext $ctx)
{
if ($ctx->hasReplied()) {
return;
}
// 匹配消息
$match_result = $this->matchBotCommand($ctx->getEvent());
if ($match_result === null) {
return;
}
// 匹配成功,检查 CommandArguments
/** @var BotCommand $command */
$command = $match_result[0];
$arguments = $this->matchCommandArguments($match_result[1], $command);
// 返回的是生成器,说明有需要询问的参数
if ($arguments instanceof \Generator) {
/** @var null|CommandArgument $argument */
$argument = $arguments->current();
if ($argument === null) {
// 是 null 表明返回了空生成器,说明参数都已经匹配完毕
$ctx->setParams($arguments->getReturn());
$this->callBotCommand($ctx, $command);
return;
}
$message = MessageSegment::text($argument->prompt === '' ? ('请输入' . $argument->name) : $argument->prompt);
$ctx->reply([$message]);
// 然后将此事件放入等待队列
self::$argument_prompt_queue[] = [$ctx->getEvent(), $arguments, $command, $match_result];
return;
}
$ctx->setParams($arguments);
// 调用方法
$this->callBotCommand($ctx, $command);
}
/**
* [CALLBACK] 处理需要等待回复的 CommandArgument
*
* @param BotContext $ctx 机器人上下文
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws OneBot12Exception
* @throws \Throwable
*/
public function handleCommandArgument(BotContext $ctx)
{
// 需要先从队列里找到定义当前会话的 prompt
// 定义一个会话的标准是:事件的 detail_typeuser_id[group_id][guild_idchannel_id] 全部相同
$new_event = $ctx->getEvent();
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) {
continue;
}
$matched = match ($old_event->detail_type) {
'private' => $new_event->getUserId() === $old_event->getUserId(),
'group' => $new_event->getGroupId() === $old_event->getGroupId(),
'guild' => $new_event->getGuildId() === $old_event->getGuildId() && $new_event->getChannelId() === $old_event->getChannelId(),
default => false,
};
if (!$matched) {
continue;
}
array_splice(self::$argument_prompt_queue, $k, 1);
// 找到了,开始处理
/** @var \Generator $arguments */
$arguments = $v[1];
$new_arguments = $arguments->send($new_event->getMessage());
if ($new_arguments !== null || $arguments->valid()) {
// 还有需要询问的参数
/** @var CommandArgument $argument */
$argument = $arguments->current();
$message = MessageSegment::text($argument->prompt === '' ? ('请输入' . $argument->name) : $argument->prompt);
$ctx->reply([$message]);
// 然后将此事件放入等待队列
self::$argument_prompt_queue[] = [$ctx->getEvent(), $arguments, $v[2], $v[3]];
} else {
// 所有参数都已经获取到了,调用方法
$ctx->setParams($arguments->getReturn());
$this->callBotCommand($ctx, $v[2]);
}
return;
}
}
/**
* [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
*/
public function handleUnknownWSReverseInput(WebSocketOpenEvent $event)
{
// 判断是不是 OneBot 12 反向 WS 连进来的,通过 Sec-WebSocket-Protocol 头
$line = explode('.', $event->getRequest()->getHeaderLine('Sec-WebSocket-Protocol'), 2);
// 如果不是 12 并且在这个最低等级事件之前还没有设置了连接信息的,一律干掉
if (empty(ConnectionUtil::getConnection($event->getFd())) && $line[0] !== '12') {
logger()->warning('不允许接入除 OneBot 12 以外的 WebSocket Client');
$event->withResponse(HttpFactory::createResponse(403, 'Forbidden'));
$event->stopPropagation();
}
}
/**
* [CALLBACK] 接入和认证反向 WS 的连接
*
* @throws StopException
* @throws \JsonException
*/
public function handleWSReverseOpen(WebSocketOpenEvent $event): void
{
// 判断是不是 OneBot 12 反向 WS 连进来的,通过 Sec-WebSocket-Protocol 头
$line = explode('.', $event->getRequest()->getHeaderLine('Sec-WebSocket-Protocol'), 2);
if ($line[0] === '12') {
logger()->info('检测到 OneBot 12 反向 WS 连接,正在进行认证...');
// 是 OneBot 12 标准的,准许接入,进行鉴权
$request = $event->getRequest();
$info = ['impl' => $line[1] ?? 'unknown'];
if (($stored_token = $event->getSocketConfig()['access_token'] ?? '') !== '') {
// 测试 Header
$token = $request->getHeaderLine('Authorization');
if ($token === '') {
// 测试 Query
$token = $request->getQueryParams()['access_token'] ?? '';
}
$token = explode('Bearer ', $token);
// 动态和静态鉴权
if ($stored_token instanceof \Closure) {
$stored_token = $stored_token($token[1] ?? null);
} else {
$stored_token = !isset($token[1]) || $token[1] !== $stored_token;
}
if (!$stored_token) { // 没有 token鉴权失败
logger()->warning('OneBot 12 反向 WS 连接鉴权失败,拒绝接入');
$event->withResponse(HttpFactory::createResponse(401, 'Unauthorized'));
$event->stopPropagation();
}
}
logger()->info('OneBot 12 反向 WS 连接鉴权成功,接入成功[' . $event->getFd() . ']');
// 接入 onebots 等实现需要回传 Sec-WebSocket-Protocol 头
$event->withResponse(HttpFactory::createResponse(101, headers: ['Sec-WebSocket-Protocol' => $event->getRequest()->getHeaderLine('Sec-WebSocket-Protocol')]));
}
// 设置 OneBot 相关的东西
ConnectionUtil::setConnection($event->getFd(), $info ?? []);
}
/**
* [CALLBACK] 处理 WebSocket 消息
*
* @param WebSocketMessageEvent $event 事件对象
* @throws OneBotException
* @throws \Throwable
*/
public function handleWSReverseMessage(WebSocketMessageEvent $event): void
{
// 忽略非 OneBot 12 的消息
$impl = ConnectionUtil::getConnection($event->getFd())['impl'] ?? null;
if ($impl === null) {
return;
}
// 解析 Frame 到 UTF-8 JSON
$body = $event->getFrame()->getData();
$body = json_decode($body, true);
if ($body === null) {
logger()->warning('收到非 JSON 格式的消息,已忽略');
return;
}
if (isset($body['type'], $body['detail_type'])) {
// 如果含有 typedetail_type 字段,表明是 event
try {
$obj = new OneBotEvent($body);
} catch (OneBotException) {
logger()->debug('收到非 OneBot 12 标准的消息,已忽略');
return;
}
// 绑定容器
ContainerRegistrant::registerOBEventServices($obj);
// 调用 BotEvent 事件
$handler = new AnnotationHandler(BotEvent::class);
$handler->setRuleCallback(function (BotEvent $event) use ($obj) {
return ($event->type === null || $event->type === $obj->type)
&& ($event->sub_type === null || $event->sub_type === $obj->sub_type)
&& ($event->detail_type === null || $event->detail_type === $obj->detail_type);
});
try {
$handler->handleAll();
} catch (WaitTimeoutException $e) {
// 这里是处理 prompt() 下超时的情况的
if ($e->getTimeoutPrompt() === null) {
return;
}
if (($e->getPromptOption() & ZM_PROMPT_TIMEOUT_MENTION_USER) === ZM_PROMPT_TIMEOUT_MENTION_USER && ($ev = $e->getUserEvent()) !== null) {
$prompt = [MessageSegment::mention($ev->getUserId()), ...$e->getTimeoutPrompt()];
}
if (($e->getPromptOption() & ZM_PROMPT_TIMEOUT_QUOTE_SELF) === ZM_PROMPT_TIMEOUT_QUOTE_SELF && ($rsp = $e->getPromptResponse()) !== null && ($ev = $e->getUserEvent()) !== null) {
$prompt = [MessageSegment::reply($rsp->data['message_id'], $ev->self['user_id']), ...$e->getTimeoutPrompt()];
} elseif (($e->getPromptOption() & ZM_PROMPT_TIMEOUT_QUOTE_USER) === ZM_PROMPT_TIMEOUT_QUOTE_USER && ($ev = $e->getUserEvent()) !== null) {
$prompt = [MessageSegment::reply($ev->getMessageId(), $ev->getUserId()), ...$e->getTimeoutPrompt()];
}
bot()->reply($prompt ?? $e->getTimeoutPrompt());
}
} elseif (isset($body['status'], $body['retcode'])) {
// 如果含有 statusretcode 字段,表明是 action 的 response
$resp = new ActionResponse();
$resp->retcode = $body['retcode'];
$resp->status = $body['status'];
$resp->message = $body['message'] ?? '';
$resp->data = $body['data'] ?? [];
$resp->echo = $body['echo'] ?? null;
ContainerRegistrant::registerOBActionResponseServices($resp);
// 调用 BotActionResponse 事件
$handler = new AnnotationHandler(BotActionResponse::class);
$handler->setRuleCallback(function (BotActionResponse $event) use ($resp) {
return $event->retcode === null || $event->retcode === $resp->retcode;
});
$handler->handleAll($resp);
// 如果有协程,并且该 echo 记录在案的话,就恢复协程
BotContext::tryResume($resp);
}
}
/**
* 根据事件匹配规则
*
* @param OneBotEvent $event 事件对象
*/
public function matchBotCommand(OneBotEvent $event): ?array
{
/** @var BotCommand[] $ls */
$ls = AnnotationMap::$_list[BotCommand::class] ?? [];
$msgs = $event->getMessage();
$head = '';
$cmd_explode = [];
$full_str = '';
foreach ($msgs as $segment) {
/** @param \MessageSegment $segment */
if ($segment->type !== 'text') {
if ($head === '') {
continue;
}
$cmd_explode[] = $segment;
continue;
}
// 当没识别到命令头的时候,就当作命令头识别
if ($head === '') {
$text = $segment->data['text'];
$full_str .= $text;
// 切分字符串
$nlp = MessageUtil::splitMessage(str_replace("\r", '', $text));
// 啥也没有,分个锤子
if (empty($nlp)) {
continue;
}
// 先预留一个给分组而配置的空间
$cmd_explode = $nlp;
$head = $nlp[0];
} else {
$full_str .= $segment->data['text'];
// 如果已经识别到了命令头,就当作命令体识别
$nlp = MessageUtil::splitMessage(str_replace("\r", '', $segment->data['text']));
if (empty($nlp)) {
continue;
}
$cmd_explode = array_merge($cmd_explode, $nlp);
}
// 先匹配
}
if ($head === '') {
return null;
}
// 遍历所有 BotCommand 注解
foreach ($ls as $v) {
/** @var BotCommand $v */
// 测试 deatil_type
if ($v->detail_type !== '' && $v->detail_type !== $event->detail_type) {
continue;
}
// 测试 prefix
if ($v->prefix !== '' && mb_strpos($full_str, $v->prefix) !== 0) {
continue;
}
// 测试 match
if ($v->match !== '' && ($v->prefix . $v->match) === $head) {
array_shift($cmd_explode);
return [$v, $cmd_explode, $full_str];
}
// 测试 alias
if ($v->match !== '' && $v->alias !== [] && in_array($head, array_map(fn ($x) => $v->prefix . $x, $v->alias), true)) {
array_shift($cmd_explode);
return [$v, $cmd_explode, $full_str];
}
// 测试 pattern
if ($v->pattern !== '' && ($args = match_args($v->pattern, $full_str)) !== false) {
return [$v, $args, $full_str];
}
// 测试 regex
if ($v->regex !== '' && preg_match('/' . $v->regex . '/u', $full_str, $match) !== 0) {
return [$v, $match, $full_str];
}
// 测试 start_with
if ($v->start_with !== '' && mb_strpos($full_str, $v->start_with) === 0) {
return [$v, [mb_substr($full_str, mb_strlen($v->start_with))], $full_str];
}
// 测试 end_with
if ($v->end_with !== '' && mb_substr($full_str, 0 - mb_strlen($v->end_with)) === $v->end_with) {
return [$v, [mb_substr($full_str, 0, 0 - mb_strlen($v->end_with))], $full_str];
}
// 测试 keyword
if ($v->keyword !== '' && mb_strpos($full_str, $v->keyword) !== false) {
return [$v, explode($v->keyword, $full_str), $full_str];
}
}
return null;
}
/**
* 根据匹配结果和 CommandArgument 进行匹配
*
* @param array $match_result 匹配结果的引用
* @param BotCommand $cmd 注解对象
* @return array|\Generator 返回 array 时为匹配结果,返回 Generator 时为等待结果
* @throws WaitTimeoutException
*/
private function matchCommandArguments(array $match_result, BotCommand $cmd): array|\Generator
{
$arguments = [];
/** @var CommandArgument $argument */
foreach ($cmd->getArguments() as $argument) {
switch ($argument->type) {
case 'string':
case 'any':
case 'str':
$cnt = count($match_result);
for ($k = 0; $k < $cnt; ++$k) {
$v = $match_result[$k];
if (is_string($v)) {
array_splice($match_result, $k, 1);
$arguments[$argument->name] = $v;
break 2;
}
}
if ($argument->required) {
// 不够用,且是必需的,就询问用户(这里可能还需要考虑没有协程环境怎么处理)
$g = yield $argument;
foreach ($g as $v) {
if ($v instanceof MessageSegment && $v->type === 'text') {
$arguments[$argument->name] = $v->data['text'];
break 2;
}
if (is_string($v)) {
$arguments[$argument->name] = $v;
break 2;
}
}
if ($argument->error_prompt_policy === 1) {
$prompt = $argument->getTypeErrorPrompt() . "\n" . $argument->prompt;
$clone_argument = clone $argument;
$clone_argument->prompt = $prompt;
$g = yield $clone_argument;
foreach ($g as $v) {
if ($v instanceof MessageSegment && $v->type === 'text') {
$arguments[$argument->name] = $v->data['text'];
break 2;
}
if (is_string($v)) {
$arguments[$argument->name] = $v;
break 2;
}
}
}
throw new WaitTimeoutException($cmd->name, $argument->getErrorQuitPrompt());
} else {
// 非必需,就使用缺省值
$arguments[$argument->name] = $argument->default;
}
break;
case 'number':
$cnt = count($match_result);
// 遍历现存的参数列表,找到第一个数字
for ($k = 0; $k < $cnt; ++$k) {
$v = $match_result[$k];
if (is_numeric($v)) {
array_splice($match_result, $k, 1);
$arguments[$argument->name] = $v / 1;
break 2;
}
}
// 找不到就看看是不是必需的,如果不是必需的,且缺省值是数字,那么就顶上
if (!$argument->required && is_numeric($argument->default)) {
$arguments[$argument->name] = $argument->default / 1;
break;
}
// 到这里还没找到,就说明需要询问用户了
$g = yield $argument;
foreach ($g as $v) {
if (is_numeric($v)) {
$arguments[$argument->name] = $v / 1;
break 2;
}
if ($v instanceof MessageSegment && $v->type === 'text') {
if (is_numeric($v->data['text'])) {
$arguments[$argument->name] = $v->data['text'] / 1;
break 2;
}
}
}
if ($argument->error_prompt_policy === 1) {
$prompt = $argument->getTypeErrorPrompt() . "\n" . $argument->prompt;
$clone_argument = clone $argument;
$clone_argument->prompt = $prompt;
$g = yield $clone_argument;
foreach ($g as $v) {
if (is_numeric($v)) {
$arguments[$argument->name] = $v / 1;
break 2;
}
if ($v instanceof MessageSegment && $v->type === 'text') {
if (is_numeric($v->data['text'])) {
$arguments[$argument->name] = $v->data['text'] / 1;
break 2;
}
}
}
}
throw new WaitTimeoutException($cmd->name, $argument->getErrorQuitPrompt());
case 'bool':
// 先遍历参数,找到具有布尔值参数的语言
$cnt = count($match_result);
for ($k = 0; $k < $cnt; ++$k) {
$v = $match_result[$k];
// 看看有没有true值
if (in_array(strtolower($v), TRUE_LIST, true)) {
array_splice($match_result, $k, 1);
$arguments[$argument->name] = true;
break 2;
}
// 看看有没有false值
if (in_array(strtolower($v), FALSE_LIST, true)) {
array_splice($match_result, $k, 1);
$arguments[$argument->name] = false;
break 2;
}
}
// 如果不是必需的,那就使用缺省值
if (!$argument->required) {
$arguments[$argument->name] = in_array($argument->default === '' ? true : 'true', TRUE_LIST);
break;
}
// 到这里还没找到,就说明需要询问用户了
$g = yield $argument;
if (in_array(strtolower($g), TRUE_LIST, true)) {
$arguments[$argument->name] = true;
} elseif (in_array(strtolower($g), FALSE_LIST, true)) {
$arguments[$argument->name] = false;
} else {
if ($argument->error_prompt_policy === 1) {
$prompt = $argument->getTypeErrorPrompt() . "\n" . $argument->prompt;
$clone_argument = clone $argument;
$clone_argument->prompt = $prompt;
$g = yield $clone_argument;
if (in_array(strtolower($g), TRUE_LIST, true)) {
$arguments[$argument->name] = true;
} elseif (in_array(strtolower($g), FALSE_LIST, true)) {
$arguments[$argument->name] = false;
} else {
throw new WaitTimeoutException($cmd->name, $argument->getErrorQuitPrompt());
}
} else {
throw new WaitTimeoutException($cmd->name, $argument->getErrorQuitPrompt());
}
}
break;
default:
// 其他类型,处理富文本
$msg_type = mb_substr($argument->type, 1);
$cnt = count($match_result);
for ($k = 0; $k < $cnt; ++$k) {
$v = $match_result[$k];
if ($v instanceof MessageSegment && $v->type === $msg_type) {
array_splice($match_result, $k, 1);
$arguments[$argument->name] = $v;
break 2;
}
}
// 如果不是必需的,那就使用缺省值
if (!$argument->required && is_array($argument->default)) {
// 生成一个消息段的段
$a = [new MessageSegment($msg_type, $argument->default)];
$arguments[$argument->name] = $a;
break;
}
// 到这里还没找到,就说明需要询问用户了
$g = yield $argument;
foreach ($g as $v) {
if ($v instanceof MessageSegment && $v->type === $msg_type) {
$arguments[$argument->name] = $v;
break 2;
}
}
if ($argument->error_prompt_policy === 1) {
$prompt = $argument->getTypeErrorPrompt() . "\n" . $argument->prompt;
$clone_argument = clone $argument;
$clone_argument->prompt = $prompt;
$g = yield $clone_argument;
foreach ($g as $v) {
if ($v instanceof MessageSegment && $v->type === $msg_type) {
$arguments[$argument->name] = $v;
break 2;
}
}
}
throw new WaitTimeoutException($cmd->name, $argument->getErrorQuitPrompt());
}
}
$arguments['.unnamed'] = $match_result;
return $arguments;
}
/**
* @throws InterruptException
* @throws \Throwable
*/
private function callBotCommand(BotContext $ctx, BotCommand $cmd)
{
$handler = new AnnotationHandler(BotCommand::class);
$handler->setReturnCallback(function ($result) use ($ctx) {
if (is_string($result) || $result instanceof MessageSegment) {
$ctx->reply($result);
return;
}
try {
Validator::validateMessageSegment($result);
$ctx->reply($result);
} catch (\Throwable) {
}
if ($ctx->hasReplied()) {
AnnotationHandler::interrupt();
}
});
$handler->handle($cmd);
}
}