add adapter feature (#121)

This commit is contained in:
sunxyw 2022-05-09 16:39:09 +08:00 committed by GitHub
parent 2481124ada
commit a6401fa9ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 363 additions and 9 deletions

View File

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace ZM\Adapters;
use Swoole\WebSocket\Frame;
use ZM\Context\ContextInterface;
interface AdapterInterface
{
/**
* 获取适配器名称
*/
public function getName(): string;
/**
* 获取适配器版本
*/
public function getVersion(): string;
/**
* 处理传入请求
*
* @param Frame $frame WebSocket消息帧
* @param ContextInterface $context 上下文
*/
public function handleIncomingRequest(Frame $frame, ContextInterface $context): void;
}

View File

@ -0,0 +1,303 @@
<?php
declare(strict_types=1);
namespace ZM\Adapters;
use Swoole\WebSocket\Frame;
use ZM\Annotation\CQ\CQAfter;
use ZM\Annotation\CQ\CQAPIResponse;
use ZM\Annotation\CQ\CQBefore;
use ZM\Annotation\CQ\CQCommand;
use ZM\Annotation\CQ\CQMessage;
use ZM\Annotation\CQ\CQMetaEvent;
use ZM\Annotation\CQ\CQNotice;
use ZM\Annotation\CQ\CQRequest;
use ZM\Config\ZMConfig;
use ZM\Context\ContextInterface;
use ZM\Event\EventDispatcher;
use ZM\Exception\WaitTimeoutException;
use ZM\Utils\CoMessage;
use ZM\Utils\MessageUtil;
class OneBot11Adapter implements AdapterInterface
{
/**
* {@inheritDoc}
*/
public function getName(): string
{
return 'onebot';
}
/**
* {@inheritDoc}
*/
public function getVersion(): string
{
return '11';
}
/**
* {@inheritDoc}
*/
public function handleIncomingRequest(Frame $frame, ContextInterface $context): void
{
$data = json_decode($frame->data, true);
// 将数据存入协程参数中
set_coroutine_params(compact('data'));
try {
// 事件类型不存在,代表为非法请求或 API 响应
if (!isset($data['post_type'])) {
if (isset($data['echo']) && CoMessage::resumeByWS()) {
EventDispatcher::interrupt();
}
$this->handleAPIResponse($data, $context);
return;
}
if ($data['post_type'] !== 'meta_event') {
$before_result = $this->handleBeforeEvent($data, 'pre');
if ($before_result->store === 'block') {
EventDispatcher::interrupt();
}
}
if (CoMessage::resumeByWS()) {
EventDispatcher::interrupt();
}
if ($data['post_type'] !== 'meta_event') {
$before_result = $this->handleBeforeEvent($data, 'post');
if ($before_result->store === 'block') {
EventDispatcher::interrupt();
}
}
switch ($data['post_type']) {
case 'message':
$this->handleMessageEvent($data, $context);
break;
case 'meta_event':
$this->handleMetaEvent($data, $context);
break;
case 'notice':
$this->handleNoticeEvent($data, $context);
break;
case 'request':
$this->handleRequestEvent($data, $context);
break;
}
if ($data['post_type'] !== 'meta_event') {
$before_result = $this->handleAfterEvent($data);
if ($before_result->store === 'block') {
EventDispatcher::interrupt();
}
}
} catch (WaitTimeoutException $e) {
$e->module->finalReply($e->getMessage());
} finally {
if (isset($data['post_type']) && $data['post_type'] !== 'meta_event') {
$before_result = $this->handleAfterEvent($data);
if ($before_result->store === 'block') {
EventDispatcher::interrupt();
}
}
}
}
/**
* 处理 API 响应
*
* @param array $data 数据
* @param ContextInterface $context 上下文
*/
private function handleAPIResponse(array $data, ContextInterface $context): void
{
set_coroutine_params(['cq_response' => $data]);
$dispatcher = new EventDispatcher(CQAPIResponse::class);
$dispatcher->setRuleFunction(function (CQAPIResponse $event) use ($context) {
return $event->retcode === $context->getCQResponse()['retcode'];
});
$dispatcher->dispatchEvents($data);
}
/**
* 处理消息事件
*
* @param array $data 消息数据
* @param ContextInterface $context 上下文
*/
private function handleMessageEvent(array $data, ContextInterface $context): void
{
// 分发 CQCommand 事件
$dispatcher = new EventDispatcher(CQCommand::class);
// 设定返回值处理函数
$dispatcher->setReturnFunction(function ($result) use ($context) {
if (is_string($result)) {
$context->reply($result);
}
if ($context->getCache('has_reply') === true) {
EventDispatcher::interrupt();
}
});
$message = $data['message'];
// 将消息段数组转换为消息字符串
if (is_array($message)) {
$message = MessageUtil::arrayToStr($message);
}
// 匹配命令
$match_result = MessageUtil::matchCommand($message, $context->getData());
if ($match_result->status) {
$matches = $match_result->match;
$input_arguments = MessageUtil::checkArguments($match_result->object->class, $match_result->object->method, $matches);
if (!empty($matches)) {
$context->setCache('match', $matches);
}
$dispatcher->dispatchEvent($match_result->object, null, ...$input_arguments);
// 处理命令返回结果
if (is_string($dispatcher->store)) {
$context->reply($dispatcher->store);
}
if ($context->getCache('has_reply') === true) {
$policy = ZMConfig::get('global', 'onebot')['message_command_policy'] ?? 'interrupt';
switch ($policy) {
case 'interrupt':
EventDispatcher::interrupt();
// no break
case 'continue':
break;
default:
throw new \Exception('未知的消息命令策略:' . $policy);
}
}
}
// 分发 CQMessage 事件
$dispatcher = new EventDispatcher(CQMessage::class);
// 设定匹配规则函数
$dispatcher->setRuleFunction(function (CQMessage $event) use ($context) {
return ($event->message === '' || ($event->message === $context->getMessage()))
&& ($event->user_id === 0 || ($event->user_id === $context->getUserId()))
&& ($event->group_id === 0 || ($event->group_id === ($context->getGroupId() ?? 0)))
&& ($event->message_type === '' || ($event->message_type === $context->getMessageType()))
&& ($event->raw_message === '' || ($event->raw_message === $context->getData()['raw_message']));
});
// 设定返回值处理函数
$dispatcher->setReturnFunction(function ($result) use ($context) {
if (is_string($result)) {
$context->reply($result);
}
});
$dispatcher->dispatchEvents($context->getMessage());
}
/**
* 处理元事件
*
* @param array $data 消息数据
* @param ContextInterface $context 上下文
*/
private function handleMetaEvent(array $data, ContextInterface $context): void
{
$dispatcher = new EventDispatcher(CQMetaEvent::class);
// 设定匹配规则函数
$dispatcher->setRuleFunction(function (CQMetaEvent $event) use ($context) {
return compare_object_and_array_by_keys($event, $context->getData(), ['meta_event_type']);
});
$dispatcher->dispatchEvents($context->getData());
}
/**
* 处理通知事件
*
* @param array $data 消息数据
* @param ContextInterface $context 上下文
*/
private function handleNoticeEvent(array $data, ContextInterface $context): void
{
$dispatcher = new EventDispatcher(CQNotice::class);
// 设定匹配规则函数
$dispatcher->setRuleFunction(function (CQNotice $event) use ($context) {
return compare_object_and_array_by_keys($event, $context->getData(), ['notice_type', 'sub_type', 'group_id', 'operator_id']);
});
$dispatcher->dispatchEvents($context->getData());
}
/**
* 处理请求事件
*
* @param array $data 消息数据
* @param ContextInterface $context 上下文
*/
private function handleRequestEvent(array $data, ContextInterface $context): void
{
$dispatcher = new EventDispatcher(CQRequest::class);
// 设定匹配规则函数
$dispatcher->setRuleFunction(function (CQRequest $event) use ($context) {
return compare_object_and_array_by_keys($event, $context->getData(), ['request_type', 'sub_type', 'user_id', 'comment']);
});
$dispatcher->dispatchEvents($context->getData());
}
/**
* 处理前置事件
*
* @param array $data 消息数据
* @param string $time 执行时机
*/
private function handleBeforeEvent(array $data, string $time): EventDispatcher
{
$dispatcher = new EventDispatcher(CQBefore::class);
// 设定匹配规则函数
$dispatcher->setRuleFunction(function (CQBefore $event) use ($data, $time) {
if ($time === 'pre') {
$level = $event->level >= 200;
} else {
$level = $event->level < 200;
}
return $level && ($event->cq_event === $data['post_type']);
});
// 设定返回值处理函数
$dispatcher->setReturnFunction(function ($result) {
if (!$result) {
EventDispatcher::interrupt('block');
}
});
$dispatcher->dispatchEvents($data);
return $dispatcher;
}
/**
* 处理后置事件
*
* @param array $data 消息数据
*/
private function handleAfterEvent(array $data): EventDispatcher
{
$dispatcher = new EventDispatcher(CQAfter::class);
// 设定匹配规则函数
$dispatcher->setRuleFunction(function (CQAfter $event) use ($data) {
return $event->cq_event === $data['post_type'];
});
$dispatcher->dispatchEvents($data);
return $dispatcher;
}
}

View File

@ -10,9 +10,11 @@ use Doctrine\Common\Annotations\AnnotationException;
use Error;
use Exception;
use Throwable;
use ZM\Adapters\OneBot11Adapter;
use ZM\Config\ZMConfig;
use ZM\Console\Console;
use ZM\Exception\InterruptException;
use ZM\Module\QQBot;
use ZM\Store\LightCacheInside;
use ZM\Store\Lock\SpinLock;
use ZM\Store\ZMAtomic;
@ -117,6 +119,11 @@ class EventDispatcher
{
try {
foreach ((EventManager::$events[$this->class] ?? []) as $v) {
// if ($v->class === QQBot::class && $v->method === 'handleByEvent') {
// zm_dump(EventManager::$events[$this->class]);
// $v->class = OneBot11Adapter::class;
// $v->method = 'handleIncomingRequest';
// }
$this->dispatchEvent($v, $this->rule, ...$params);
if ($this->log) {
Console::verbose("[事件分发{$this->eid}] 单一对象 " . $v->class . '::' . (is_string($v->method) ? $v->method : '{closure}') . ' 分发结束。');

View File

@ -14,6 +14,8 @@ use Swoole\Coroutine;
use Swoole\Database\PDOConfig;
use Swoole\Process;
use Swoole\WebSocket\Server;
use ZM\Adapters\AdapterInterface;
use ZM\Adapters\OneBot11Adapter;
use ZM\Annotation\AnnotationParser;
use ZM\Annotation\Swoole\OnMessageEvent;
use ZM\Annotation\Swoole\OnStart;
@ -30,7 +32,6 @@ use ZM\Event\SwooleEvent;
use ZM\Exception\DbException;
use ZM\Exception\ZMKnownException;
use ZM\Framework;
use ZM\Module\QQBot;
use ZM\MySQL\MySQLPool;
use ZM\Store\LightCacheInside;
use ZM\Store\MySQL\SqlPoolStorage;
@ -215,8 +216,8 @@ class OnWorkerStart implements SwooleEvent
Console::debug('OneBot support enabled, listening OneBot event(3).');
$obj = new OnMessageEvent();
$obj->connect_type = 'qq';
$obj->class = QQBot::class;
$obj->method = 'handleByEvent';
$obj->class = AdapterInterface::class;
$obj->method = 'handleIncomingRequest';
$obj->level = $obb_onebot['message_level'] ?? 99;
EventManager::addEvent(OnMessageEvent::class, $obj);
if ($obb_onebot['single_bot_mode']) {
@ -310,5 +311,7 @@ class OnWorkerStart implements SwooleEvent
// 基础
$container->instance('server', $server);
$container->instance('worker_id', $server->worker_id);
$container->singleton(AdapterInterface::class, OneBot11Adapter::class);
}
}

View File

@ -10,7 +10,6 @@ use ZM\Console\Console;
use ZM\Framework;
use ZM\Store\Lock\SpinLock;
use ZM\Store\ZMAtomic;
use ZM\Store\ZMBuf;
use function file_get_contents;
use function get_included_files;
use function is_callable;
@ -51,11 +50,7 @@ class ZMUtil
public static function getModInstance($class)
{
if (!isset(ZMBuf::$instance[$class])) {
// Console::debug('Class instance $class not exist, so I created it.');
return ZMBuf::$instance[$class] = new $class();
}
return ZMBuf::$instance[$class];
return resolve($class);
}
/**

View File

@ -759,6 +759,23 @@ function app(string $abstract = null, array $parameters = [])
return resolve($abstract, $parameters);
}
/**
* 根据键名比较对象和数组
*
* @param object $object 对象
* @param array $array 数组
* @param array $keys 键名
*/
function compare_object_and_array_by_keys(object $object, array $array, array $keys): bool
{
foreach ($keys as $key) {
if (!isset($object->{$key}, $array[$key]) || $object->{$key} !== $array[$key]) {
return false;
}
}
return true;
}
/**
* 以下为废弃的函数,将于未来移除
*/