mirror of
https://github.com/zhamao-robot/zhamao-framework.git
synced 2026-03-18 05:04:51 +08:00
commit
61b48676ae
@ -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');
|
||||
|
||||
21
src/Globals/global_class_alias_helper.php
Normal file
21
src/Globals/global_class_alias_helper.php
Normal 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');
|
||||
@ -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('', '');
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
33
src/ZM/Annotation/OneBot/BotAction.php
Normal file
33
src/ZM/Annotation/OneBot/BotAction.php
Normal 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;
|
||||
}
|
||||
}
|
||||
36
src/ZM/Annotation/OneBot/BotActionResponse.php
Normal file
36
src/ZM/Annotation/OneBot/BotActionResponse.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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(
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
28
src/ZM/Command/Generate/ClassAliasHelperGenerateCommand.php
Normal file
28
src/ZM/Command/Generate/ClassAliasHelperGenerateCommand.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -73,4 +73,12 @@ class ClassAliasHelper
|
||||
{
|
||||
return self::$aliases[$alias]['class'] ?? $alias;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有别名定义信息
|
||||
*/
|
||||
public static function getAllAlias(): array
|
||||
{
|
||||
return self::$aliases;
|
||||
}
|
||||
}
|
||||
|
||||
134
src/ZM/Context/BotContext.php
Normal file
134
src/ZM/Context/BotContext.php
Normal 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.');
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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));
|
||||
|
||||
9
src/ZM/Exception/OneBot12Exception.php
Normal file
9
src/ZM/Exception/OneBot12Exception.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZM\Exception;
|
||||
|
||||
class OneBot12Exception extends PluginException
|
||||
{
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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'])) {
|
||||
// 如果含有 type,detail_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'])) {
|
||||
// 如果含有 status,retcode 字段,表明是 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 [];
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
85
src/ZM/Utils/CatCode.php
Normal 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(['&', '[', ']'], ['&', '[', ']'], (string) $msg);
|
||||
if ($is_content) {
|
||||
$msg = str_replace(',', ',', $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(['&', '[', ']'], ['&', '[', ']'], (string) $msg);
|
||||
if ($is_content) {
|
||||
$msg = str_replace(',', ',', $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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
76
src/ZM/Utils/MessageUtil.php
Normal file
76
src/ZM/Utils/MessageUtil.php
Normal 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]);
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user