diff --git a/docs/guide/errcode.md b/docs/guide/errcode.md index cf3adff8..be68b41a 100644 --- a/docs/guide/errcode.md +++ b/docs/guide/errcode.md @@ -78,4 +78,5 @@ | E00074 | 参数非法 | 检查调用的参数是否正常(此处可能有多处问题,请看具体调用栈炸掉的地方) | | E00075 | Cron表达式非法 | 检查 @Cron 中的表达式是否书写格式正确 | | E00076 | Cron检查间隔非法 | 检查 @Cron 中的检查间隔(毫秒)是否在1000-60000之间 | +| E00077 | 输入了非法的消息匹配参数类型 | 检查传入 `@CommandArgument` 或 `checkArguments()` 方法有没有传入非法的 `type` 参数 | | E99999 | 未知错误 | | diff --git a/src/Module/Example/Hello.php b/src/Module/Example/Hello.php index 4766b02d..02d97154 100644 --- a/src/Module/Example/Hello.php +++ b/src/Module/Example/Hello.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Module\Example; +use ZM\Annotation\CQ\CommandArgument; use ZM\Annotation\CQ\CQBefore; use ZM\Annotation\CQ\CQCommand; use ZM\Annotation\CQ\CQMessage; @@ -136,19 +137,18 @@ class Hello * 问法2:从1到20的随机数 * @CQCommand("随机数") * @CQCommand(pattern="*从*到*的随机数") - * + * @CommandArgument(name="num1",type="int",required=true) + * @CommandArgument(name="num2",type="int",required=true) + * @param mixed $num1 + * @param mixed $num2 * @return string */ - public function randNum() + public function randNum($num1, $num2) { - // 获取第一个数字类型的参数 - $num1 = ctx()->getNumArg('请输入第一个数字'); - // 获取第二个数字类型的参数 - $num2 = ctx()->getNumArg('请输入第二个数字'); - $a = min(intval($num1), intval($num2)); - $b = max(intval($num1), intval($num2)); + $a = min($num1, $num2); + $b = max($num1, $num2); // 回复用户结果 - return '随机数是:' . mt_rand($a, $b); + return '从' . $a . '到' . $b . '的随机数是:' . mt_rand($a, $b); } /** diff --git a/src/ZM/Annotation/AnnotationParser.php b/src/ZM/Annotation/AnnotationParser.php index 1bfcfb07..227d1de7 100644 --- a/src/ZM/Annotation/AnnotationParser.php +++ b/src/ZM/Annotation/AnnotationParser.php @@ -21,6 +21,7 @@ use ZM\Annotation\Interfaces\Level; use ZM\Annotation\Module\Closed; use ZM\Config\ZMConfig; use ZM\Console\Console; +use ZM\Event\EventManager; use ZM\Exception\AnnotationException; use ZM\Utils\Manager\RouteManager; use ZM\Utils\ZMUtil; @@ -156,11 +157,12 @@ class AnnotationParser } $inserted[$v][$method_name] = true; } - } - if ($method_anno instanceof RequestMapping) { + } elseif ($method_anno instanceof RequestMapping) { RouteManager::importRouteByAnnotation($method_anno, $method_name, $v, $methods_annotations); } elseif ($method_anno instanceof Middleware) { $this->middleware_map[$method_anno->class][$method_anno->method][] = $method_anno; + } else { + EventManager::$event_map[$method_anno->class][$method_anno->method][] = $method_anno; } } } diff --git a/src/ZM/Annotation/CQ/CommandArgument.php b/src/ZM/Annotation/CQ/CommandArgument.php new file mode 100644 index 00000000..38030822 --- /dev/null +++ b/src/ZM/Annotation/CQ/CommandArgument.php @@ -0,0 +1,146 @@ +name = $name; + $this->description = $description; + $this->type = $this->fixTypeName($type); + $this->required = $required; + $this->prompt = $prompt; + $this->default = $default; + $this->timeout = $timeout; + $this->error_prompt_policy = $error_prompt_policy; + if ($this->type === 'bool') { + if ($this->default === '') { + $this->default = 'yes'; + } + if (!in_array($this->default, array_merge(TRUE_LIST, FALSE_LIST))) { + throw new InvalidArgumentException('CommandArgument参数 ' . $name . ' 类型传入类型应为布尔型,检测到非法的默认值 ' . $this->default); + } + } elseif ($this->type === 'number') { + if ($this->default === '') { + $this->default = '0'; + } + if (!is_numeric($this->default)) { + throw new InvalidArgumentException('CommandArgument参数 ' . $name . ' 类型传入类型应为数字型,检测到非法的默认值 ' . $this->default); + } + } + } + + public function getTypeErrorPrompt(): string + { + return '参数类型错误,请重新输入!'; + } + + public function getErrorQuitPrompt(): string + { + return '参数类型错误,停止输入!'; + } + + /** + * @throws ZMKnownException + */ + protected function fixTypeName(string $type): string + { + $table = [ + 'str' => 'string', + 'string' => 'string', + 'strings' => 'string', + 'byte' => 'string', + 'num' => 'number', + 'number' => 'number', + 'int' => 'number', + 'float' => 'number', + 'double' => 'number', + 'boolean' => 'bool', + 'bool' => 'bool', + 'true' => 'bool', + 'any' => 'any', + 'all' => 'any', + '*' => 'any', + ]; + if (array_key_exists($type, $table)) { + return $table[$type]; + } + throw new ZMKnownException(zm_internal_errcode('E00077') . 'Invalid argument type: ' . $type . ', only support any, string, number and bool !'); + } +} diff --git a/src/ZM/Entity/InputArguments.php b/src/ZM/Entity/InputArguments.php new file mode 100644 index 00000000..cef8d148 --- /dev/null +++ b/src/ZM/Entity/InputArguments.php @@ -0,0 +1,30 @@ +arguments = $arguments; + } + + public function getArguments(): array + { + return $this->arguments; + } + + public function getArgument($name) + { + return $this->arguments[$name] ?? null; + } + + public function get($name) + { + return $this->getArgument($name); + } +} diff --git a/src/ZM/Event/EventManager.php b/src/ZM/Event/EventManager.php index 02ab9334..09b60da4 100644 --- a/src/ZM/Event/EventManager.php +++ b/src/ZM/Event/EventManager.php @@ -23,6 +23,8 @@ class EventManager public static $middleware_map = []; + public static $event_map = []; + public static $middlewares = []; public static $req_mapping = []; @@ -33,6 +35,7 @@ class EventManager Console::debug("Adding event {$event_name} at @Anonymous"); } else { Console::debug("Adding event {$event_name} at " . ($event_obj->class) . ':' . ($event_obj->method)); + self::$event_map[$event_obj->class][$event_obj->method][] = $event_obj; } self::$events[$event_name][] = $event_obj; (new AnnotationParser())->sortByLevel(self::$events, $event_name); diff --git a/src/ZM/Event/EventMapIterator.php b/src/ZM/Event/EventMapIterator.php new file mode 100644 index 00000000..ea52243f --- /dev/null +++ b/src/ZM/Event/EventMapIterator.php @@ -0,0 +1,61 @@ +class = $class; + $this->method = $method; + $this->event_name = $event_name; + $this->nextToValid(); + } + + public function current() + { + return EventManager::$event_map[$this->class][$this->method][$this->offset]; + } + + public function next(): void + { + ++$this->offset; + $this->nextToValid(); + } + + #[ReturnTypeWillChange] + public function key() + { + return $this->offset; + } + + public function valid(): bool + { + return isset(EventManager::$event_map[$this->class][$this->method][$this->offset]); + } + + public function rewind(): void + { + $this->offset = 0; + } + + private function nextToValid() + { + while ($this->valid() && !is_a($this->current(), $this->event_name, true)) { + ++$this->offset; + } + } +} diff --git a/src/ZM/Module/QQBot.php b/src/ZM/Module/QQBot.php index f398aed4..368339ca 100644 --- a/src/ZM/Module/QQBot.php +++ b/src/ZM/Module/QQBot.php @@ -161,10 +161,13 @@ class QQBot } $s = MessageUtil::matchCommand($msg, ctx()->getData()); if ($s->status !== false) { - if (!empty($s->match)) { - ctx()->setCache('match', $s->match); + $match = $s->match; + + $input_arguments = MessageUtil::checkArguments($s->object->class, $s->object->method, $match); + if (!empty($match)) { + ctx()->setCache('match', $match); } - $dispatcher->dispatchEvent($s->object, null); + $dispatcher->dispatchEvent($s->object, null, ...$input_arguments); if (is_string($dispatcher->store)) { ctx()->reply($dispatcher->store); } diff --git a/src/ZM/Utils/MessageUtil.php b/src/ZM/Utils/MessageUtil.php index dba96f55..16217602 100644 --- a/src/ZM/Utils/MessageUtil.php +++ b/src/ZM/Utils/MessageUtil.php @@ -8,12 +8,16 @@ use Exception; use Iterator; use ReflectionException; use ReflectionMethod; +use ZM\Annotation\CQ\CommandArgument; use ZM\Annotation\CQ\CQCommand; use ZM\API\CQ; use ZM\Config\ZMConfig; use ZM\Console\Console; +use ZM\Entity\InputArguments; use ZM\Entity\MatchResult; use ZM\Event\EventManager; +use ZM\Event\EventMapIterator; +use ZM\Exception\WaitTimeoutException; use ZM\Requests\ZMRequest; use ZM\Store\WorkerCache; use ZM\Utils\Manager\WorkerManager; @@ -327,4 +331,96 @@ class MessageUtil WorkerCache::set('command_helps', $helps); return $helps; } + + /** + * @throws WaitTimeoutException + */ + public static function checkArguments(string $class, string $method, array &$match): array + { + $iterator = new EventMapIterator($class, $method, CommandArgument::class); + $offset = 0; + $arguments = []; + foreach ($iterator as $annotation) { + /** @var CommandArgument $annotation */ + switch ($annotation->type) { + case 'string': + case 'any': + if (isset($match[$offset])) { + $arguments[$annotation->name] = $match[$offset++]; + } else { + if ($annotation->required) { + $value = ctx()->waitMessage($annotation->prompt === '' ? ('请输入' . $annotation->name) : $annotation->prompt, $annotation->timeout); + $arguments[$annotation->name] = $value; + } else { + $arguments[$annotation->name] = $annotation->default; + } + } + break; + case 'number': + for ($k = $offset; $k < count($match); ++$k) { + $v = $match[$k]; + if (is_numeric($v)) { + array_splice($match, $k, 1); + $arguments[$annotation->name] = $v / 1; + break 2; + } + } + if (!$annotation->required) { + if (is_numeric($annotation->default)) { + $arguments[$annotation->name] = $annotation->default / 1; + } + } + if (!isset($arguments[$annotation->name])) { + $value = ctx()->waitMessage($annotation->prompt === '' ? ('请输入' . $annotation->name) : $annotation->prompt, $annotation->timeout); + if (!is_numeric($value)) { + if ($annotation->error_prompt_policy === 1) { + $value = ctx()->waitMessage($annotation->getTypeErrorPrompt(), $annotation->timeout); + if (!is_numeric($value)) { + throw new WaitTimeoutException(ctx(), $annotation->getErrorQuitPrompt()); + } + } else { + throw new WaitTimeoutException(ctx(), $annotation->getErrorQuitPrompt()); + } + } + $arguments[$annotation->name] = $value / 1; + } + break; + case 'bool': + for ($k = $offset; $k < count($match); ++$k) { + $v = strtolower($match[$k]); + if (in_array(strtolower($v), TRUE_LIST)) { + array_splice($match, $k, 1); + $arguments[$annotation->name] = true; + break 2; + } + if (in_array(strtolower($v), FALSE_LIST)) { + array_splice($match, $k, 1); + $arguments[$annotation->name] = false; + break 2; + } + } + if (!$annotation->required) { + $default = $annotation->default === '' ? true : 'true'; + $arguments[$annotation->name] = in_array($default, TRUE_LIST); + } + if (!isset($arguments[$annotation->name])) { + $value = strtolower(ctx()->waitMessage($annotation->prompt === '' ? ('请输入' . $annotation->name) : $annotation->prompt, $annotation->timeout)); + if (!in_array($value, array_merge(TRUE_LIST, FALSE_LIST))) { + if ($annotation->error_prompt_policy === 1) { + $value = strtolower(ctx()->waitMessage($annotation->getTypeErrorPrompt(), $annotation->timeout)); + if (!in_array($value, array_merge(TRUE_LIST, FALSE_LIST))) { + throw new WaitTimeoutException(ctx(), $annotation->getErrorQuitPrompt()); + } + } else { + throw new WaitTimeoutException(ctx(), $annotation->getErrorQuitPrompt()); + } + } + $arguments[$annotation->name] = in_array($value, TRUE_LIST); + } + break; + } + } + container()->instance(InputArguments::class, new InputArguments($arguments)); + return $arguments; + } } diff --git a/src/ZM/global_defines.php b/src/ZM/global_defines.php index 8af53448..9d9dd956 100644 --- a/src/ZM/global_defines.php +++ b/src/ZM/global_defines.php @@ -17,6 +17,9 @@ if (!is_dir(CRASH_DIR)) { @mkdir(CRASH_DIR); } +const TRUE_LIST = ['yes', 'y', 'true', 'on', '是', '对', true]; +const FALSE_LIST = ['no', 'n', 'false', 'off', '否', '错', false]; + const CONN_WEBSOCKET = 0; const CONN_HTTP = 1; const ZM_MATCH_ALL = 0;