Merge pull request #188 from zhamao-robot/v3-beta1

Beta 1 发布
This commit is contained in:
Jerry 2022-12-20 23:45:43 +08:00 committed by GitHub
commit 61b48676ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 639 additions and 44 deletions

View File

@ -15,6 +15,8 @@ ClassAliasHelper::addAlias(\ZM\Annotation\OneBot\BotEvent::class, 'BotEvent');
ClassAliasHelper::addAlias(\ZM\Annotation\OneBot\CommandArgument::class, 'CommandArgument');
ClassAliasHelper::addAlias(\ZM\Annotation\Closed::class, 'Closed');
ClassAliasHelper::addAlias(\ZM\Plugin\ZMPlugin::class, 'ZMPlugin');
ClassAliasHelper::addAlias(\OneBot\V12\Object\OneBotEvent::class, 'OneBotEvent');
ClassAliasHelper::addAlias(\ZM\Context\BotContext::class, 'BotContext');
// 下面是 OneBot 相关类的全局别称
ClassAliasHelper::addAlias(\OneBot\Driver\Event\WebSocket\WebSocketOpenEvent::class, 'WebSocketOpenEvent');

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
class_alias(ZM\Annotation\Framework\BindEvent::class, 'BindEvent');
class_alias(ZM\Annotation\Framework\Init::class, 'Init');
class_alias(ZM\Annotation\Framework\Setup::class, 'Setup');
class_alias(ZM\Annotation\Http\Controller::class, 'Controller');
class_alias(ZM\Annotation\Http\Route::class, 'Route');
class_alias(ZM\Annotation\Middleware\Middleware::class, 'Middleware');
class_alias(ZM\Annotation\OneBot\BotCommand::class, 'BotCommand');
class_alias(ZM\Annotation\OneBot\BotEvent::class, 'BotEvent');
class_alias(ZM\Annotation\OneBot\CommandArgument::class, 'CommandArgument');
class_alias(ZM\Annotation\Closed::class, 'Closed');
class_alias(ZM\Plugin\ZMPlugin::class, 'ZMPlugin');
class_alias(OneBot\V12\Object\OneBotEvent::class, 'OneBotEvent');
class_alias(ZM\Context\BotContext::class, 'BotContext');
class_alias(OneBot\Driver\Event\WebSocket\WebSocketOpenEvent::class, 'WebSocketOpenEvent');
class_alias(OneBot\Driver\Event\WebSocket\WebSocketCloseEvent::class, 'WebSocketCloseEvent');
class_alias(OneBot\Driver\Event\WebSocket\WebSocketMessageEvent::class, 'WebSocketMessageEvent');
class_alias(OneBot\Driver\Event\Http\HttpRequestEvent::class, 'HttpRequestEvent');

View File

@ -6,11 +6,11 @@ use OneBot\Driver\Coroutine\Adaptive;
use OneBot\Driver\Coroutine\CoroutineInterface;
use OneBot\Driver\Process\ExecutionResult;
use OneBot\V12\Object\MessageSegment;
use OneBot\V12\Object\OneBotEvent;
use Psr\Log\LoggerInterface;
use ZM\Config\ZMConfig;
use ZM\Container\Container;
use ZM\Container\ContainerInterface;
use ZM\Context\Context;
use ZM\Logger\ConsoleLogger;
use ZM\Middleware\MiddlewareHandler;
use ZM\Store\Database\DBException;
@ -68,6 +68,9 @@ function zm_internal_errcode(int|string $code): string
return "[ErrCode:{$code}] ";
}
/**
* 返回当前炸毛实例的 ID
*/
function zm_instance_id(): string
{
if (defined('ZM_INSTANCE_ID')) {
@ -101,11 +104,6 @@ function is_assoc_array(array $array): bool
return !empty($array) && array_keys($array) !== range(0, count($array) - 1);
}
function ctx(): Context
{
return \container()->get('ctx');
}
/**
* 构建消息段的助手函数
*
@ -208,3 +206,13 @@ function config(array|string $key = null, mixed $default = null)
}
return $config->get($key, $default);
}
function bot(): ZM\Context\BotContext
{
if (\container()->has('bot.event')) {
/** @var OneBotEvent $bot_event */
$bot_event = \container()->get('bot.event');
return new \ZM\Context\BotContext($bot_event->self['user_id'] ?? '', $bot_event->self['platform']);
}
return new \ZM\Context\BotContext('', '');
}

View File

@ -41,7 +41,7 @@ class AnnotationHandler
public function __construct(string $annotation_class)
{
$this->annotation_class = $annotation_class;
logger()->debug('开始分发注解 {annotation}', ['annotation' => $annotation_class]);
logger()->debug('声明注解分发器 {annotation}', ['annotation' => $annotation_class]);
}
/**
@ -88,6 +88,7 @@ class AnnotationHandler
*/
public function handleAll(mixed ...$params)
{
logger()->debug('开始分发注解 ' . $this->annotation_class);
try {
// 遍历注册的注解
foreach ((AnnotationMap::$_list[$this->annotation_class] ?? []) as $v) {

View File

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace ZM\Annotation\OneBot;
use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor;
use Doctrine\Common\Annotations\Annotation\Target;
use ZM\Annotation\AnnotationBase;
use ZM\Annotation\Interfaces\Level;
/**
* @Annotation
* @NamedArgumentConstructor()
* @Target("METHOD")
*/
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
class BotAction extends AnnotationBase implements Level
{
public function __construct(public string $action = '', public bool $need_response = false, public int $level = 20)
{
}
public function getLevel()
{
return $this->level;
}
public function setLevel($level)
{
$this->level = $level;
}
}

View File

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace ZM\Annotation\OneBot;
use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor;
use Doctrine\Common\Annotations\Annotation\Target;
use ZM\Annotation\AnnotationBase;
use ZM\Annotation\Interfaces\Level;
/**
* Class BotActionResponse
* 机器人指令注解
*
* @Annotation
* @NamedArgumentConstructor()
* @Target("METHOD")
*/
#[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_METHOD)]
class BotActionResponse extends AnnotationBase implements Level
{
public function __construct(public ?int $retcode = null, public int $level = 20)
{
}
public function getLevel()
{
return $this->level;
}
public function setLevel($level)
{
$this->level = $level;
}
}

View File

@ -27,8 +27,20 @@ class BotCommand extends AnnotationBase implements Level
/**
* @param string[] $alias
*/
public function __construct(public $name = '', public $match = '', public $pattern = '', public $regex = '', public $start_with = '', public $end_with = '', public $keyword = '', public $alias = [], public $message_type = '', public $user_id = '', public $group_id = '', public $level = 20)
{
public function __construct(
public $name = '',
public $match = '',
public $pattern = '',
public $regex = '',
public $start_with = '',
public $end_with = '',
public $keyword = '',
public $alias = [],
public $detail_type = '',
public $user_id = '',
public $group_id = '',
public $level = 20
) {
}
public static function make(

View File

@ -8,6 +8,7 @@ use Attribute;
use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor;
use Doctrine\Common\Annotations\Annotation\Target;
use ZM\Annotation\AnnotationBase;
use ZM\Annotation\Interfaces\Level;
/**
* 机器人相关事件注解
@ -17,20 +18,28 @@ use ZM\Annotation\AnnotationBase;
* @NamedArgumentConstructor()
*/
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
class BotEvent extends AnnotationBase
class BotEvent extends AnnotationBase implements Level
{
public function __construct(public ?string $type = null, public ?string $detail_type = null, public ?string $impl = null, public ?string $platform = null, public ?string $self_id = null, public ?string $sub_type = null)
public function __construct(public ?string $type = null, public ?string $detail_type = null, public ?string $sub_type = null, public int $level = 20)
{
}
public static function make(
?string $type = null,
?string $detail_type = null,
?string $impl = null,
?string $platform = null,
?string $self_id = null,
?string $sub_type = null
?string $sub_type = null,
int $level = 20,
): BotEvent {
return new static(...func_get_args());
}
public function getLevel(): int
{
return $this->level;
}
public function setLevel($level)
{
$this->level = $level;
}
}

View File

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace ZM\Command\Generate;
use Symfony\Component\Console\Attribute\AsCommand;
use ZM\Command\Command;
use ZM\Container\ClassAliasHelper;
#[AsCommand(name: 'generate:alias-helper', description: '类别名的 IDE Helper 文件生成')]
class ClassAliasHelperGenerateCommand extends Command
{
/**
* {@inheritDoc}
*/
protected function handle(): int
{
$str = "<?php\n\ndeclare(strict_types=1);\n\n";
$alias = ClassAliasHelper::getAllAlias();
foreach ($alias as $a => $c) {
$str .= "class_alias({$c['class']}::class, '{$a}');\n";
}
file_put_contents(FRAMEWORK_ROOT_DIR . '/src/Globals/global_class_alias_helper.php', $str);
$this->info('生成成功');
return Command::SUCCESS;
}
}

View File

@ -73,4 +73,12 @@ class ClassAliasHelper
{
return self::$aliases[$alias]['class'] ?? $alias;
}
/**
* 获取所有别名定义信息
*/
public static function getAllAlias(): array
{
return self::$aliases;
}
}

View File

@ -0,0 +1,134 @@
<?php
declare(strict_types=1);
namespace ZM\Context;
use Choir\Http\HttpFactory;
use OneBot\Driver\Event\Http\HttpRequestEvent;
use OneBot\Driver\Event\WebSocket\WebSocketMessageEvent;
use OneBot\Util\Utils;
use OneBot\V12\Object\Action;
use OneBot\V12\Object\MessageSegment;
use OneBot\V12\Object\OneBotEvent;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use ZM\Annotation\AnnotationHandler;
use ZM\Annotation\OneBot\BotAction;
use ZM\Exception\OneBot12Exception;
use ZM\Utils\MessageUtil;
class BotContext implements ContextInterface
{
private static array $echo_id_list = [];
private array $self;
public function __construct(string $bot_id, string $platform)
{
$this->self = ['user_id' => $bot_id, 'platform' => $platform];
}
public function getEvent(): OneBotEvent
{
return container()->get('bot.event');
}
/**
* 快速回复机器人消息文本
*
* @param array|MessageSegment|string|\Stringable $message 消息内容、消息段或消息段数组
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws OneBot12Exception
* @throws \Throwable
*/
public function reply(\Stringable|MessageSegment|array|string $message)
{
if (container()->has('bot.event')) {
// 这里直接使用当前上下文的事件里面的参数,不再重新挨个获取怎么发消息的参数
/** @var OneBotEvent $event */
$event = container()->get('bot.event');
// reply 的条件是必须 type=message
if ($event->getType() !== '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));
return $this->sendMessage($msg, $event->detail_type, $event->jsonSerialize());
}
throw new OneBot12Exception('bot()->reply() can only be used in message event.');
}
/**
* 返回是否已经调用过回复了
*/
public function hasReplied(): bool
{
// TODO: 完成是否已经回复的记录和返回
return false;
}
/**
* 获取其他机器人的上下文操作对象
*
* @param string $bot_id 机器人的 self.user_id 对应的 ID
* @param string $platform 机器人的 self.platform 对应的 platform
* @return $this
*/
public function getBot(string $bot_id, string $platform = ''): BotContext
{
// TODO: 完善多机器人支持
return $this;
}
/**
* @throws \Throwable
*/
public function sendMessage(\Stringable|array|MessageSegment|string $message, string $detail_type, array $params = [])
{
$message = MessageUtil::convertToArr($message);
$params['message'] = $message;
$params['detail_type'] = $detail_type;
return $this->sendAction(Utils::camelToSeparator(__FUNCTION__), $params, $this->self);
}
public function getEchoAction(mixed $echo): ?Action
{
return self::$echo_id_list[$echo] ?? null;
}
/**
* @throws \Throwable
*/
private function sendAction(string $action, array $params = [], ?array $self = null)
{
// 声明 Action 对象
$a = new Action($action, $params, ob_uuidgen(), $self);
self::$echo_id_list[$a->echo] = $a;
// 调用事件在回复之前的回调
$handler = new AnnotationHandler(BotAction::class);
$handler->setRuleCallback(fn (BotAction $act) => $act->action === $action && !$act->need_response);
$handler->handleAll($a);
// 被阻断时候,就不发送了
if ($handler->getStatus() === AnnotationHandler::STATUS_INTERRUPTED) {
return false;
}
// 调用机器人连接发送 Action
if (container()->has('ws.message.event')) {
/** @var WebSocketMessageEvent $ws */
$ws = container()->get('ws.message.event');
return $ws->send(json_encode($a->jsonSerialize()));
}
// 如果是 HTTP WebHook 的形式,那么直接调用 Response
if (container()->has('http.request.event')) {
/** @var HttpRequestEvent $event */
$event = container()->get('http.request.event');
$response = HttpFactory::createResponse(headers: ['Content-Type' => 'application/json'], body: json_encode([$a->jsonSerialize()]));
$event->withResponse($response);
return true;
}
throw new OneBot12Exception('No bot connection found.');
}
}

View File

@ -4,27 +4,6 @@ declare(strict_types=1);
namespace ZM\Context;
use OneBot\Driver\Event\Http\HttpRequestEvent;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
interface ContextInterface
{
/**
* 获取 Http Request 请求对象
*/
public function getRequest(): ServerRequestInterface;
/**
* 获取 Http 请求事件对象
*/
public function getHttpRequestEvent(): HttpRequestEvent;
/**
* 使用 Response 对象响应 Http 请求
* Wrapper of HttpRequestEvent::withResponse method
*
* @param ResponseInterface $response 响应对象
*/
public function withResponse(ResponseInterface $response);
}

View File

@ -12,6 +12,7 @@ use OneBot\Util\Singleton;
use ZM\Annotation\AnnotationHandler;
use ZM\Annotation\Framework\BindEvent;
use ZM\Container\ContainerServicesProvider;
use ZM\Exception\Handler;
use ZM\Utils\ConnectionUtil;
class WSEventListener
@ -23,7 +24,6 @@ class WSEventListener
*/
public function onWebSocketOpen(WebSocketOpenEvent $event): void
{
logger()->info('接入连接: ' . $event->getFd());
// 计数,最多只能接入 1024 个连接,为了适配多进程
if (!ConnectionUtil::addConnection($event->getFd(), [])) {
$event->withResponse(HttpFactory::createResponse(503));
@ -31,19 +31,32 @@ class WSEventListener
}
// 注册容器
resolve(ContainerServicesProvider::class)->registerServices('connection');
container()->instance(WebSocketOpenEvent::class, $event);
container()->alias(WebSocketOpenEvent::class, 'ws.open.event');
// 调用注解
$handler = new AnnotationHandler(BindEvent::class);
$handler->setRuleCallback(fn ($x) => is_a($x->event_class, WebSocketOpenEvent::class, true));
$handler->handleAll($event);
resolve(ContainerServicesProvider::class)->cleanup();
}
public function onWebSocketMessage(WebSocketMessageEvent $event): void
{
container()->instance(WebSocketMessageEvent::class, $event);
container()->alias(WebSocketMessageEvent::class, 'ws.message.event');
// 调用注解
$handler = new AnnotationHandler(BindEvent::class);
$handler->setRuleCallback(fn ($x) => is_a($x->event_class, WebSocketMessageEvent::class, true));
$handler->handleAll($event);
try {
$handler = new AnnotationHandler(BindEvent::class);
$handler->setRuleCallback(fn ($x) => is_a($x->event_class, WebSocketMessageEvent::class, true));
$handler->handleAll();
} catch (\Throwable $e) {
logger()->error("处理 WebSocket 消息时出现异常:{$e->getMessage()}");
Handler::getInstance()->handle($e);
} finally {
resolve(ContainerServicesProvider::class)->cleanup();
}
}
/**
@ -52,6 +65,9 @@ class WSEventListener
public function onWebSocketClose(WebSocketCloseEvent $event): void
{
logger()->info('关闭连接: ' . $event->getFd());
// 绑定容器
container()->instance(WebSocketCloseEvent::class, $event);
container()->alias(WebSocketCloseEvent::class, 'ws.close.event');
// 调用注解
$handler = new AnnotationHandler(BindEvent::class);
$handler->setRuleCallback(fn ($x) => is_a($x->event_class, WebSocketCloseEvent::class, true));

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace ZM\Exception;
class OneBot12Exception extends PluginException
{
}

View File

@ -45,7 +45,7 @@ class Framework
public const VERSION_ID = 637;
/** @var string 版本名称 */
public const VERSION = '3.0.0-alpha5';
public const VERSION = '3.0.0-beta1';
/** @var array 传入的参数 */
protected array $argv;

View File

@ -6,10 +6,21 @@ namespace ZM\Plugin;
use Choir\Http\HttpFactory;
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\OneBotEvent;
use OneBot\V12\Validator;
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\ContainerServicesProvider;
use ZM\Context\BotContext;
use ZM\Utils\ConnectionUtil;
class OneBot12Adapter extends ZMPlugin
@ -21,7 +32,11 @@ class OneBot12Adapter extends ZMPlugin
case '':
case 'onebot12':
// 处理所有 OneBot 12 的反向 WS 握手事件
$this->addEvent(WebSocketOpenEvent::class, [$this, 'handleWSReverseInput']);
$this->addEvent(WebSocketOpenEvent::class, [$this, 'handleWSReverseOpen']);
$this->addEvent(\WebSocketMessageEvent::class, [$this, 'handleWSReverseMessage']);
// 在 BotEvent 内处理 BotCommand
// $cmd_event = BotEvent::make(type: 'message', level: 15)->on([$this, 'handleBotCommand']);
// $this->addBotEvent($cmd_event);
// 处理和声明所有 BotCommand 下的 CommandArgument
$parser->addSpecialParser(BotCommand::class, [$this, 'parseBotCommand']);
// 不需要给列表写入 CommandArgument
@ -61,6 +76,33 @@ class OneBot12Adapter extends ZMPlugin
return true;
}
/**
* 调用 BotCommand 注解的方法
*
* @param BotEvent $event BotEvent 事件
* @param BotContext $ctx 机器人环境上下文
*/
public function handleBotCommand(BotEvent $event, BotContext $ctx)
{
$handler = new AnnotationHandler(BotCommand::class);
$handler->setReturnCallback(function ($result) use ($ctx) {
if (is_string($result)) {
$ctx->reply($result);
return;
}
try {
Validator::validateMessageSegment($result);
$ctx->reply($result);
} catch (\Throwable) {
}
if ($ctx->hasReplied()) {
AnnotationHandler::interrupt();
}
});
// 匹配消息
$match_result = $this->matchBotCommand($ctx->getEvent());
}
/**
* @throws StopException
*/
@ -79,7 +121,7 @@ class OneBot12Adapter extends ZMPlugin
* 接入和认证反向 WS 的连接
* @throws StopException
*/
public function handleWSReverseInput(WebSocketOpenEvent $event): void
public function handleWSReverseOpen(WebSocketOpenEvent $event): void
{
// 判断是不是 OneBot 12 反向 WS 连进来的,通过 Sec-WebSocket-Protocol 头
$line = explode('.', $event->getRequest()->getHeaderLine('Sec-WebSocket-Protocol'), 2);
@ -87,6 +129,7 @@ class OneBot12Adapter extends ZMPlugin
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');
@ -95,15 +138,91 @@ class OneBot12Adapter extends ZMPlugin
$token = $request->getQueryParams()['access_token'] ?? '';
}
$token = explode('Bearer ', $token);
$info = ['impl' => $line[1] ?? 'unknown'];
if (!isset($token[1]) || $token[1] !== $stored_token) { // 没有 token鉴权失败
logger()->warning('OneBot 12 反向 WS 连接鉴权失败,拒绝接入');
$event->withResponse(HttpFactory::createResponse(401, 'Unauthorized'));
$event->stopPropagation();
}
}
logger()->info('OneBot 12 反向 WS 连接鉴权成功,接入成功[' . $event->getFd() . ']');
}
// 设置 OneBot 相关的东西
ConnectionUtil::setConnection($event->getFd(), $info ?? []);
}
/**
* 处理 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;
}
// 处理
resolve(ContainerServicesProvider::class)->registerServices('message');
// 解析 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 $e) {
logger()->debug('收到非 OneBot 12 标准的消息,已忽略');
return;
}
// 绑定容器
container()->instance(OneBotEvent::class, $obj);
container()->alias(OneBotEvent::class, 'bot.event');
container()->bind(BotContext::class, function () { return bot(); });
// 调用 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);
});
$handler->handleAll($obj);
} 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'] ?? null;
container()->instance(ActionResponse::class, $resp);
container()->alias(ActionResponse::class, 'bot.action.response');
// 调用 BotActionResponse 事件
$handler = new AnnotationHandler(BotActionResponse::class);
$handler->setRuleCallback(function (BotActionResponse $event) use ($resp) {
return $event->retcode === null || $event->retcode === $resp->retcode;
});
$handler->handleAll($resp);
}
}
private function matchBotCommand(OneBotEvent $event): array
{
$ls = AnnotationMap::$_list[BotCommand::class] ?? [];
$msg = $event->getMessageString();
// TODO: 还没写完匹配 BotCommand
return [];
}
}

View File

@ -43,6 +43,9 @@ class PluginManager
public static function addPluginsFromDir(string $dir): int
{
// 遍历插件目录
if (!is_dir($dir)) {
return 0;
}
$list = FileSystem::scanDirFiles($dir, false, false, true);
$cnt = 0;
foreach ($list as $item) {
@ -159,6 +162,11 @@ class PluginManager
throw new PluginException('插件 ' . $meta['name'] . ' 无法加载,因为没有入口文件,也没有自动加载文件和内建 Composer');
}
/**
* 启用所有插件
*
* @param AnnotationParser $parser 传入注解解析器,用于将插件中的事件注解解析出来
*/
public static function enablePlugins(AnnotationParser $parser): void
{
foreach (self::$plugins as $name => $plugin) {

85
src/ZM/Utils/CatCode.php Normal file
View File

@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace ZM\Utils;
use OneBot\V12\Object\MessageSegment;
class CatCode
{
/**
* MessageSegment 转换为 CatCode 字符串
*/
public static function fromSegment(mixed $message_segment): string
{
// 传入的必须是段数组或段对象
if (is_array($message_segment)) {
$str = '';
foreach ($message_segment as $v) {
if (!$v instanceof MessageSegment) {
return '';
}
$str .= self::segment2CatCode($v);
}
return $str;
}
if ($message_segment instanceof MessageSegment) {
return self::segment2CatCode($message_segment);
}
if (is_string($message_segment)) {
return $message_segment;
}
return '';
}
/**
* 转义CatCode的特殊字符
*
* @param int|string|\Stringable $msg 字符串
* @param bool $is_content 如果是转义CatCode本体内容则为false默认如果是参数内的字符串则为true
* @return string 转义后的CatCode
*/
public static function encode(\Stringable|int|string $msg, bool $is_content = false): string
{
$msg = str_replace(['&', '[', ']'], ['&amp;', '&#91;', '&#93;'], (string) $msg);
if ($is_content) {
$msg = str_replace(',', '&#44;', $msg);
}
return $msg;
}
/**
* 反转义字符串中的CatCode敏感符号
*
* @param int|string|\Stringable $msg 字符串
* @param bool $is_content 如果是解码CatCode本体内容则为false默认如果是参数内的字符串则为true
* @return string 转义后的CatCode
*/
public static function decode(\Stringable|int|string $msg, bool $is_content = false): string
{
$msg = str_replace(['&amp;', '&#91;', '&#93;'], ['&', '[', ']'], (string) $msg);
if ($is_content) {
$msg = str_replace('&#44;', ',', $msg);
}
return $msg;
}
/**
* 转换一个 Segment CatCode
*
* @param MessageSegment $segment 段对象
*/
private static function segment2CatCode(MessageSegment $segment): string
{
if ($segment->type === 'text') {
return $segment->data['text'];
}
$str = '[CatCode:' . $segment->type;
foreach ($segment->data as $key => $value) {
$str .= ',' . $key . '=' . self::encode($value, true);
}
$str .= ']';
return $str;
}
}

View File

@ -72,4 +72,15 @@ class ConnectionUtil
@unlink(zm_dir(ZM_STATE_DIR . '/.WS' . $fd . '.' . ProcessManager::getProcessId()));
}
}
/**
* 获取记录连接内容的特殊信息
*
* @param int $fd WS 连接 ID
* @return null|mixed
*/
public static function getConnection(int $fd)
{
return self::$connection_handles[$fd] ?? null;
}
}

View File

@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace ZM\Utils;
use OneBot\V12\Object\MessageSegment;
/**
* 机器人消息处理工具类
*/
class MessageUtil
{
/**
* 将消息段无损转换为 CatCode 字符串
*
* @param array $message_segment 消息段
*/
public static function arrayToStr(array $message_segment): string
{
return CatCode::fromSegment($message_segment);
}
/**
* 将含有 CatCode 字符串的消息文本无损转换为消息段数组
*
* @param string $msg 字符串消息(包含 CatCode 的)
* @param bool $assoc_result 是否返回关联数组形式。当值为 True 时,返回的是数组形式,否则返回 MessageSegment[] 对象列表形式(默认为 False
* @param bool $ignore_space 是否忽略空行(默认为 True
* @param bool $trim_text 是否去除空格文本(默认为 False
* @return array|MessageSegment[]
*/
public static function strToArray(string $msg, bool $assoc_result = false, bool $ignore_space = true, bool $trim_text = false): array
{
$arr = [];
while (($rear = mb_strstr($msg, '[CatCode:')) !== false && ($end = mb_strstr($rear, ']', true)) !== false) {
// 把 [CatCode: 前面的文字生成段落
$front = mb_strstr($msg, '[CatCode:', true);
// 如果去掉空格都还有文字,或者不去掉空格有字符,且不忽略空格,则生成段落,否则不生成
if (($trim_front = trim($front)) !== '' || ($front !== '' && !$ignore_space)) {
$text = CatCode::decode($trim_text ? $trim_front : $front);
$arr[] = $assoc_result ? ['type' => 'text', 'data' => ['text' => $text]] : new MessageSegment('text', ['text' => $text]);
}
// 处理 CatCode
$content = mb_substr($end, 4);
$cq = explode(',', $content);
$object_type = array_shift($cq);
$object_params = [];
foreach ($cq as $v) {
$key = mb_strstr($v, '=', true);
$object_params[$key] = CatCode::decode(mb_substr(mb_strstr($v, '='), 1), true);
}
$arr[] = $assoc_result ? ['type' => $object_type, 'data' => $object_params] : new MessageSegment($object_type, $object_params);
$msg = mb_substr(mb_strstr($rear, ']'), 1);
}
if (($trim_msg = trim($msg)) !== '' || ($msg !== '' && !$ignore_space)) {
$text = CatCode::decode($trim_text ? $trim_msg : $msg);
$arr[] = $assoc_result ? ['type' => 'text', 'data' => ['text' => $text]] : new MessageSegment('text', ['text' => $text]);
}
return $arr;
}
public static function convertToArr(MessageSegment|\Stringable|array|string $message)
{
if (is_array($message)) {
return $message;
}
if ($message instanceof MessageSegment) {
return [$message];
}
if ($message instanceof \Stringable) {
return new MessageSegment('text', ['text' => $message->__toString()]);
}
return new MessageSegment('text', ['text' => $message]);
}
}