Merge pull request #240 from zhamao-robot/beta5-update

Beta5 系列升级内容(包含 Redis、sendAction 协程、reply 模式、修复 data 报错)
This commit is contained in:
Jerry 2023-01-04 21:38:17 +08:00 committed by GitHub
commit cd85bd087c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 345 additions and 18 deletions

View File

@ -31,6 +31,11 @@ const ZM_ERR_METHOD_NOT_FOUND = 1; // 找不到方法
const ZM_ERR_ROUTE_NOT_FOUND = 2; // 找不到路由
const ZM_ERR_ROUTE_METHOD_NOT_ALLOWED = 3; // 路由方法不允许
/** 定义 BotContext 下 reply 回复的模式 */
const ZM_REPLY_NONE = 0; // 默认回复,不带任何东西
const ZM_REPLY_MENTION = 1; // 回复时 @ 该用户
const ZM_REPLY_QUOTE = 2; // 回复时引用该消息
const LOAD_MODE_VENDOR = 0; // 从 vendor 加载
const LOAD_MODE_SRC = 1; // 从 src 加载

View File

@ -15,6 +15,7 @@ use ZM\Store\Database\DBException;
use ZM\Store\Database\DBQueryBuilder;
use ZM\Store\Database\DBWrapper;
use ZM\Store\KV\KVInterface;
use ZM\Store\KV\Redis\RedisWrapper;
// 防止重复引用引发报错
if (function_exists('zm_internal_errcode')) {
@ -216,6 +217,16 @@ function sql_builder(string $name = ''): DBQueryBuilder
return (new DBWrapper($name))->createQueryBuilder();
}
/**
* 获取 Redis 操作类
*
* @param string $name 使用的 Redis 连接名称
*/
function redis(string $name = 'default'): RedisWrapper
{
return new RedisWrapper($name);
}
/**
* 获取 / 设置配置项
*

View File

@ -4,9 +4,10 @@ declare(strict_types=1);
namespace ZM\Context;
use DI\DependencyException;
use DI\NotFoundException;
use OneBot\Driver\Event\Http\HttpRequestEvent;
use OneBot\Driver\Event\WebSocket\WebSocketMessageEvent;
use OneBot\V12\Object\Action;
use OneBot\V12\Object\MessageSegment;
use OneBot\V12\Object\OneBotEvent;
use ZM\Context\Trait\BotActionTrait;
@ -17,14 +18,16 @@ class BotContext implements ContextInterface
{
use BotActionTrait;
/** @var array<string, array<string, BotContext>> 记录机器人的上下文列表 */
private static array $bots = [];
private static array $echo_id_list = [];
/** @var string[] 记录当前上下文绑定的机器人 */
private array $self;
/** @var array 如果是 BotCommand 匹配的上下文,这里会存放匹配到的参数 */
private array $params = [];
/** @var bool 用于标记当前上下文会话是否已经调用过 reply() 方法 */
private bool $replied = false;
public function __construct(string $bot_id, string $platform, null|WebSocketMessageEvent|HttpRequestEvent $event = null)
@ -34,6 +37,12 @@ class BotContext implements ContextInterface
$this->base_event = $event;
}
/**
* 获取机器人事件对象
*
* @throws DependencyException
* @throws NotFoundException
*/
public function getEvent(): OneBotEvent
{
return container()->get('bot.event');
@ -42,9 +51,10 @@ class BotContext implements ContextInterface
/**
* 快速回复机器人消息文本
*
* @param array|MessageSegment|string|\Stringable $message 消息内容、消息段或消息段数组
* @param array|MessageSegment|string|\Stringable $message 消息内容、消息段或消息段数组
* @param int $reply_mode 回复消息模式,默认为空,可选 ZM_REPLY_MENTIONat 用户、ZM_REPLY_QUOTE引用消息
*/
public function reply(\Stringable|MessageSegment|array|string $message)
public function reply(\Stringable|MessageSegment|array|string $message, int $reply_mode = ZM_REPLY_NONE)
{
if (container()->has('bot.event')) {
// 这里直接使用当前上下文的事件里面的参数,不再重新挨个获取怎么发消息的参数
@ -52,11 +62,13 @@ class BotContext implements ContextInterface
$event = container()->get('bot.event');
// reply 的条件是必须 type=message
if ($event->getType() !== 'message') {
if ($event->type !== 'message') {
throw new OneBot12Exception('bot()->reply() can only be used in message event.');
}
$msg = (is_string($message) ? [new MessageSegment('text', ['text' => $message])] : ($message instanceof MessageSegment ? [$message] : $message));
$this->replied = true;
// 判断规则
$this->matchReplyMode($reply_mode, $msg, $event);
return $this->sendMessage($msg, $event->detail_type, $event->jsonSerialize());
}
throw new OneBot12Exception('bot()->reply() can only be used in message event.');
@ -126,13 +138,25 @@ class BotContext implements ContextInterface
return $this->params;
}
public function getEchoAction(mixed $echo): ?Action
{
return self::$echo_id_list[$echo] ?? null;
}
public function getSelf(): array
{
return $this->self;
}
/**
* 匹配更改 reply 的特殊模式
*
* @param int $reply_mode 回复模式
* @param array $message_segments 消息段的引用
* @param OneBotEvent $event 事件对象
*/
private function matchReplyMode(int $reply_mode, array &$message_segments, OneBotEvent $event)
{
if (($reply_mode & ZM_REPLY_QUOTE) === ZM_REPLY_QUOTE) {
array_unshift($message_segments, new MessageSegment('reply', ['message_id' => $event->getMessageId(), 'user_id' => $event->getUserId()]));
}
if (($reply_mode & ZM_REPLY_MENTION) === ZM_REPLY_MENTION) {
array_unshift($message_segments, new MessageSegment('mention', ['user_id' => $event->getUserId()]));
}
}
}

View File

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace ZM\Context\Trait;
use Choir\Http\HttpFactory;
use OneBot\Driver\Coroutine\Adaptive;
use OneBot\Driver\Event\Http\HttpRequestEvent;
use OneBot\Driver\Event\WebSocket\WebSocketMessageEvent;
use OneBot\Util\Utils;
@ -18,9 +19,28 @@ use ZM\Utils\MessageUtil;
trait BotActionTrait
{
/**
* @var array<string, int> 一个记录 echo 对应协程 ID 的列表,用于恢复协程
*/
private static array $coroutine_list = [];
private null|WebSocketMessageEvent|HttpRequestEvent $base_event;
/**
* @internal 只允许内部调用
* @param ActionResponse $response 尝试调用看看有没有协程等待的
*/
public static function tryResume(ActionResponse $response): void
{
if (($co = Adaptive::getCoroutine()) !== null && isset(self::$coroutine_list[$response->echo ?? ''])) {
$co->resume(self::$coroutine_list[$response->echo ?? ''], $response);
}
}
/**
* 发送一条机器人消息
*
* @param array|MessageSegment|string|\Stringable $message 消息内容,可以是消息段、字符串
* @throws \Throwable
*/
public function sendMessage(\Stringable|array|MessageSegment|string $message, string $detail_type, array $params = []): ActionResponse|bool
@ -40,7 +60,6 @@ trait BotActionTrait
{
// 声明 Action 对象
$a = new Action($action, $params, ob_uuidgen(), $self);
self::$echo_id_list[$a->echo] = $a;
// 调用事件在回复之前的回调
$handler = new AnnotationHandler(BotAction::class);
container()->set(Action::class, $a);
@ -51,7 +70,7 @@ trait BotActionTrait
return false;
}
// 调用机器人连接发送 Action
// 调用机器人连接发送 Action,首先试试看是不是 WebSocket
if ($this->base_event instanceof WebSocketMessageEvent) {
$result = $this->base_event->send(json_encode($a->jsonSerialize()));
}
@ -69,13 +88,19 @@ trait BotActionTrait
container()->get('http.request.event')->withResponse($response);
$result = true;
}
// 如果开启了协程,并且成功发送,那就进入协程等待,挂起等待结果返回一个 ActionResponse 对象
if (($result ?? false) === true && ($co = Adaptive::getCoroutine()) !== null) {
self::$coroutine_list[$a->echo] = $co->getCid();
$response = $co->suspend();
if ($response instanceof ActionResponse) {
return $response;
}
return false;
}
if (isset($result)) {
return $result;
}
/* TODO: 协程支持
if (($result ?? false) === true && ($co = Adaptive::getCoroutine()) !== null) {
return $result ?? false;
}*/
// 到这里表明你调用时候不在 WS 或 HTTP 上下文
throw new OneBot12Exception('No bot connection found.');
}
}

View File

@ -283,7 +283,8 @@ class OneBot12Adapter extends ZMPlugin
$resp->retcode = $body['retcode'];
$resp->status = $body['status'];
$resp->message = $body['message'] ?? '';
$resp->data = $body['data'] ?? null;
$resp->data = $body['data'] ?? [];
$resp->echo = $body['echo'] ?? null;
ContainerRegistrant::registerOBActionResponseServices($resp);
@ -293,6 +294,9 @@ class OneBot12Adapter extends ZMPlugin
return $event->retcode === null || $event->retcode === $resp->retcode;
});
$handler->handleAll($resp);
// 如果有协程,并且该 echo 记录在案的话,就恢复协程
BotContext::tryResume($resp);
}
}

View File

@ -0,0 +1,258 @@
<?php
declare(strict_types=1);
namespace ZM\Store\KV\Redis;
/**
* Redis 对象操作的 IDE Helper
*
* @method acl($subcmd, $args)
* @method append($key, $value)
* @method auth($auth)
* @method bgSave()
* @method bgrewriteaof()
* @method bitcount($key)
* @method bitop($operation, $ret_key, $key, $other_keys)
* @method bitpos($key, $bit, $start, $end)
* @method blPop($key, $timeout_or_key, $extra_args)
* @method brPop($key, $timeout_or_key, $extra_args)
* @method brpoplpush($src, $dst, $timeout)
* @method bzPopMax($key, $timeout_or_key, $extra_args)
* @method bzPopMin($key, $timeout_or_key, $extra_args)
* @method clearLastError()
* @method client($cmd, $args)
* @method close()
* @method command($args)
* @method config($cmd, $key, $value)
* @method connect($host, $port, $timeout, $retry_interval)
* @method dbSize()
* @method debug($key)
* @method decr($key)
* @method decrBy($key, $value)
* @method del($key, $other_keys)
* @method discard()
* @method dump($key)
* @method echo($msg)
* @method eval($script, $args, $num_keys)
* @method evalsha($script_sha, $args, $num_keys)
* @method exec()
* @method exists($key, $other_keys)
* @method expire($key, $timeout)
* @method expireAt($key, $timestamp)
* @method flushAll($async)
* @method flushDB($async)
* @method geoadd($key, $lng, $lat, $member, $other_triples)
* @method geodist($key, $src, $dst, $unit)
* @method geohash($key, $member, $other_members)
* @method geopos($key, $member, $other_members)
* @method georadius($key, $lng, $lan, $radius, $unit, $opts)
* @method georadius_ro($key, $lng, $lan, $radius, $unit, $opts)
* @method georadiusbymember($key, $member, $radius, $unit, $opts)
* @method georadiusbymember_ro($key, $member, $radius, $unit, $opts)
* @method get($key)
* @method getAuth()
* @method getBit($key, $offset)
* @method getDBNum()
* @method getHost()
* @method getLastError()
* @method getMode()
* @method getOption($option)
* @method getPersistentID()
* @method getPort()
* @method getRange($key, $start, $end)
* @method getReadTimeout()
* @method getSet($key, $value)
* @method getTimeout()
* @method hDel($key, $member, $other_members)
* @method hExists($key, $member)
* @method hGet($key, $member)
* @method hGetAll($key)
* @method hIncrBy($key, $member, $value)
* @method hIncrByFloat($key, $member, $value)
* @method hKeys($key)
* @method hLen($key)
* @method hMget($key, $keys)
* @method hMset($key, $pairs)
* @method hSet($key, $member, $value)
* @method hSetNx($key, $member, $value)
* @method hStrLen($key, $member)
* @method hVals($key)
* @method hscan($str_key, $i_iterator, $str_pattern, $i_count)
* @method incr($key)
* @method incrBy($key, $value)
* @method incrByFloat($key, $value)
* @method info($option)
* @method isConnected()
* @method keys($pattern)
* @method lInsert($key, $position, $pivot, $value)
* @method lLen($key)
* @method lPop($key)
* @method lPush($key, $value)
* @method lPushx($key, $value)
* @method lSet($key, $index, $value)
* @method lastSave()
* @method lindex($key, $index)
* @method lrange($key, $start, $end)
* @method lrem($key, $value, $count)
* @method ltrim($key, $start, $stop)
* @method mget($keys)
* @method migrate($host, $port, $key, $db, $timeout, $copy, $replace)
* @method move($key, $dbindex)
* @method mset($pairs)
* @method msetnx($pairs)
* @method multi($mode)
* @method object($field, $key)
* @method pconnect($host, $port, $timeout)
* @method persist($key)
* @method pexpire($key, $timestamp)
* @method pexpireAt($key, $timestamp)
* @method pfadd($key, $elements)
* @method pfcount($key)
* @method pfmerge($dstkey, $keys)
* @method ping()
* @method pipeline()
* @method psetex($key, $expire, $value)
* @method psubscribe($patterns, $callback)
* @method pttl($key)
* @method publish($channel, $message)
* @method pubsub($cmd, $args)
* @method punsubscribe($pattern, $other_patterns)
* @method rPop($key)
* @method rPush($key, $value)
* @method rPushx($key, $value)
* @method randomKey()
* @method rawcommand($cmd, $args)
* @method rename($key, $newkey)
* @method renameNx($key, $newkey)
* @method restore($ttl, $key, $value)
* @method role()
* @method rpoplpush($src, $dst)
* @method sAdd($key, $value)
* @method sAddArray($key, $options)
* @method sDiff($key, $other_keys)
* @method sDiffStore($dst, $key, $other_keys)
* @method sInter($key, $other_keys)
* @method sInterStore($dst, $key, $other_keys)
* @method sMembers($key)
* @method sMisMember($key, $member, $other_members)
* @method sMove($src, $dst, $value)
* @method sPop($key)
* @method sRandMember($key, $count)
* @method sUnion($key, $other_keys)
* @method sUnionStore($dst, $key, $other_keys)
* @method save()
* @method scan($i_iterator, $str_pattern, $i_count)
* @method scard($key)
* @method script($cmd, $args)
* @method select($dbindex)
* @method set($key, $value, $opts)
* @method setBit($key, $offset, $value)
* @method setOption($option, $value)
* @method setRange($key, $offset, $value)
* @method setex($key, $expire, $value)
* @method setnx($key, $value)
* @method sismember($key, $value)
* @method slaveof($host, $port)
* @method slowlog($arg, $option)
* @method sort($key, $options)
* @method sortAsc($key, $pattern, $get, $start, $end, $getList)
* @method sortAscAlpha($key, $pattern, $get, $start, $end, $getList)
* @method sortDesc($key, $pattern, $get, $start, $end, $getList)
* @method sortDescAlpha($key, $pattern, $get, $start, $end, $getList)
* @method srem($key, $member, $other_members)
* @method sscan($str_key, $i_iterator, $str_pattern, $i_count)
* @method strlen($key)
* @method subscribe($channels, $callback)
* @method swapdb($srcdb, $dstdb)
* @method time()
* @method ttl($key)
* @method type($key)
* @method unlink($key, $other_keys)
* @method unsubscribe($channel, $other_channels)
* @method unwatch()
* @method wait($numslaves, $timeout)
* @method watch($key, $other_keys)
* @method xack($str_key, $str_group, $arr_ids)
* @method xadd($str_key, $str_id, $arr_fields, $i_maxlen, $boo_approximate)
* @method xclaim($str_key, $str_group, $str_consumer, $i_min_idle, $arr_ids, $arr_opts)
* @method xdel($str_key, $arr_ids)
* @method xgroup($str_operation, $str_key, $str_arg1, $str_arg2, $str_arg3)
* @method xinfo($str_cmd, $str_key, $str_group)
* @method xlen($key)
* @method xpending($str_key, $str_group, $str_start, $str_end, $i_count, $str_consumer)
* @method xrange($str_key, $str_start, $str_end, $i_count)
* @method xread($arr_streams, $i_count, $i_block)
* @method xreadgroup($str_group, $str_consumer, $arr_streams, $i_count, $i_block)
* @method xrevrange($str_key, $str_start, $str_end, $i_count)
* @method xtrim($str_key, $i_maxlen, $boo_approximate)
* @method zAdd($key, $score, $value, $extra_args)
* @method zCard($key)
* @method zCount($key, $min, $max)
* @method zIncrBy($key, $value, $member)
* @method zLexCount($key, $min, $max)
* @method zPopMax($key)
* @method zPopMin($key)
* @method zRange($key, $start, $end, $scores)
* @method zRangeByLex($key, $min, $max, $offset, $limit)
* @method zRangeByScore($key, $start, $end, $options)
* @method zRank($key, $member)
* @method zRem($key, $member, $other_members)
* @method zRemRangeByLex($key, $min, $max)
* @method zRemRangeByRank($key, $start, $end)
* @method zRemRangeByScore($key, $min, $max)
* @method zRevRange($key, $start, $end, $scores)
* @method zRevRangeByLex($key, $min, $max, $offset, $limit)
* @method zRevRangeByScore($key, $start, $end, $options)
* @method zRevRank($key, $member)
* @method zScore($key, $member)
* @method zinterstore($key, $keys, $weights, $aggregate)
* @method zscan($str_key, $i_iterator, $str_pattern, $i_count)
* @method zunionstore($key, $keys, $weights, $aggregate)
* @method delete($key, $other_keys)
* @method evaluate($script, $args, $num_keys)
* @method evaluateSha($script_sha, $args, $num_keys)
* @method getKeys($pattern)
* @method getMultiple($keys)
* @method lGet($key, $index)
* @method lGetRange($key, $start, $end)
* @method lRemove($key, $value, $count)
* @method lSize($key)
* @method listTrim($key, $start, $stop)
* @method open($host, $port, $timeout, $retry_interval)
* @method popen($host, $port, $timeout)
* @method renameKey($key, $newkey)
* @method sContains($key, $value)
* @method sGetMembers($key)
* @method sRemove($key, $member, $other_members)
* @method sSize($key)
* @method sendEcho($msg)
* @method setTimeout($key, $timeout)
* @method substr($key, $start, $end)
* @method zDelete($key, $member, $other_members)
* @method zDeleteRangeByRank($key, $min, $max)
* @method zDeleteRangeByScore($key, $min, $max)
* @method zInter($key, $keys, $weights, $aggregate)
* @method zRemove($key, $member, $other_members)
* @method zRemoveRangeByScore($key, $min, $max)
* @method zReverseRange($key, $start, $end, $scores)
* @method zSize($key)
* @method zUnion($key, $keys, $weights, $aggregate)
*/
class RedisWrapper
{
public function __construct(private string $pool = 'default')
{
}
public function __call(string $name, array $arguments)
{
/** @var ZMRedis $pool */
$pool = RedisPool::pool($this->pool)->get();
if (method_exists($pool, $name)) {
$result = $pool->{$name}(...$arguments);
}
RedisPool::pool($this->pool)->put($pool);
return $result ?? false;
}
}