add @CommandArgument annotation

* add @CommandArgument and relevant event changes

* fix unknown bug

* remove relative constant for CommandArgument
This commit is contained in:
Jerry Ma 2022-05-03 10:06:57 +08:00 committed by GitHub
parent a393b3aff5
commit d19d7acedd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 359 additions and 14 deletions

View File

@ -78,4 +78,5 @@
| E00074 | 参数非法 | 检查调用的参数是否正常(此处可能有多处问题,请看具体调用栈炸掉的地方) |
| E00075 | Cron表达式非法 | 检查 @Cron 中的表达式是否书写格式正确 |
| E00076 | Cron检查间隔非法 | 检查 @Cron 中的检查间隔毫秒是否在1000-60000之间 |
| E00077 | 输入了非法的消息匹配参数类型 | 检查传入 `@CommandArgument``checkArguments()` 方法有没有传入非法的 `type` 参数 |
| E99999 | 未知错误 | |

View File

@ -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);
}
/**

View File

@ -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;
}
}
}

View File

@ -0,0 +1,146 @@
<?php
declare(strict_types=1);
namespace ZM\Annotation\CQ;
use Attribute;
use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor;
use Doctrine\Common\Annotations\Annotation\Required;
use ZM\Annotation\AnnotationBase;
use ZM\Annotation\Interfaces\ErgodicAnnotation;
use ZM\Exception\InvalidArgumentException;
use ZM\Exception\ZMKnownException;
/**
* Class CommandArgument
* @Annotation
* @NamedArgumentConstructor()
* @Target("ALL")
*/
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_ALL)]
class CommandArgument extends AnnotationBase implements ErgodicAnnotation
{
/**
* @var string
* @Required()
*/
public $name;
/**
* @var string
*/
public $description = '';
/**
* @var string
*/
public $type = 'string';
/**
* @var bool
*/
public $required = false;
/**
* @var string
*/
public $prompt = '';
/**
* @var string
*/
public $default = '';
/**
* @var int
*/
public $timeout = 60;
/**
* @var int
*/
public $error_prompt_policy = 1;
/**
* @param string $name 参数名称(可以是中文)
* @param string $description 参数描述(默认为空)
* @param bool $required 参数是否必需如果是必需为true默认为false
* @param string $prompt 当参数为必需时,返回给用户的提示输入的消息(默认为"请输入$name"
* @param string $default 当required为false时未匹配到参数将自动使用default值默认为空
* @param int $timeout prompt超时时间默认为60秒
* @throws InvalidArgumentException|ZMKnownException
*/
public function __construct(
string $name,
string $description = '',
string $type = 'string',
bool $required = false,
string $prompt = '',
string $default = '',
int $timeout = 60,
int $error_prompt_policy = 1
) {
$this->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 !');
}
}

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace ZM\Entity;
class InputArguments
{
private $arguments;
public function __construct(array $arguments)
{
$this->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);
}
}

View File

@ -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);

View File

@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace ZM\Event;
use Iterator;
use ReturnTypeWillChange;
class EventMapIterator implements Iterator
{
private $offset = 0;
private $class;
private $method;
private $event_name;
public function __construct($class, $method, $event_name)
{
$this->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;
}
}
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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;