mirror of
https://github.com/zhamao-robot/go-cqhttp-adapter-plugin.git
synced 2026-03-17 12:14:51 +08:00
initial commit
This commit is contained in:
commit
baee59b025
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
/vendor/
|
||||
composer.lock
|
||||
/.idea/
|
||||
/zm_data/
|
||||
74
README.md
Normal file
74
README.md
Normal file
@ -0,0 +1,74 @@
|
||||
# go-cqhttp-adapter-plugin
|
||||
|
||||
炸毛框架用于接入 go-cqhttp(OneBot 11)的适配器插件。
|
||||
|
||||
## 功能
|
||||
|
||||
该插件将 gocq 的反向 WebSocket 接入信息全部转换为 OneBot 12 标准,安装该插件后几乎无需修改任何代码即可接入。
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
# Composer 安装稳定版
|
||||
composer require zhamao/go-cqhttp-adapter-plugin
|
||||
|
||||
# GitHub 安装 Nightly 版
|
||||
./zhamao plugin:install https://github.com/zhamao-robot/go-cqhttp-adapter-plugin.git
|
||||
```
|
||||
|
||||
## 转换注意事项
|
||||
|
||||
由于 OneBot 11 和 OneBot 12 有较多差异,而这些差异也导致两者不能无损相互转换。
|
||||
例如 OneBot 11 中未规定要求文件分片上传和下载的动作,那么在使用本插件时也无法使用这些动作。
|
||||
|
||||
由于 OneBot 11 的实现较多,而且和 OneBot 11 本身相差较大,该插件也时重点针对 go-cqhttp 进行适配转换,这也是插件不叫 onebot-11-adapter 的原因。
|
||||
|
||||
## 事件转换规则(11 转 12)
|
||||
|
||||
下面是 `post_type` 转换规则:
|
||||
|
||||
- 字段名 `post_type` 转换为 `type`。
|
||||
- 如果 `post_type` 值为 `meta_event`,转换为 `meta`。
|
||||
- 如果 `post_type` 值为 `message_sent`,转换为 `message`。
|
||||
|
||||
下面是 `XXX_type` 转换规则:
|
||||
|
||||
- 如果 `post_type` 为 `message` 或 `message_sent`,将字段名 `message_type` 转换为 `detail_type`。
|
||||
- 如果 `post_type` 为 `meta_event`,将字段名 `meta_event_type` 转换为 `detail_type`。
|
||||
- 如果 `post_type` 为 `notice`,将字段名 `notice_type` 转换为 `detail_type`。
|
||||
- 如果 `post_type` 为 `request`,将字段名 `request_type` 转换为 `detail_type`。
|
||||
|
||||
下面是 `self_id` 转换规则:
|
||||
|
||||
- 如果存在 `self_id` 字段,将该字段删除,替换为 `"self" => ["user_id" => $user_id, "platform" => "qq"]`,其中 `$user_id` 的值等于 `self_id` 的字符串值。
|
||||
- 如果不存在 `self_id` 字段,将该字段删除,替换为和上方一样的格式,`$user_id` 的值等于该连接请求握手时 `X-Self-ID` 的值。
|
||||
|
||||
下面是 `user_id`、`group_id`、`guild_id`、`channel_id`、`message_id` 转换规则:
|
||||
|
||||
- 以上列举的值都取字符串值,即转换为字符串。
|
||||
|
||||
下面是其他字段的一些转换规则:
|
||||
|
||||
- 如果 `post_type` 为 `message` 或 `message_sent`,则将 `raw_message` 转换为 `alt_message`。
|
||||
- go-cqhttp 的消息事件中默认带有 `sender`,将其字段名转换为 `qq.sender`,内部参数不变。
|
||||
|
||||
下面是通知事件(`notice`)的一些转换规则:
|
||||
|
||||
| go-cqhttp 的 `notice_type` | OneBot 12 的 `detail_type` | 描述 |
|
||||
|---------------------------|---------------------------|-----------|
|
||||
| `friend_recall` | `private_message_delete` | 撤回一条私聊消息 |
|
||||
| `friend_add` | `friend_increase` | 添加好友的通知事件 |
|
||||
| `group_increase` | `group_member_increase` | 群成员增加 |
|
||||
| `group_decrease` | `group_member_decrease` | 群成员减少 |
|
||||
| `group_recall` | `group_message_delete` | 群消息撤回 |
|
||||
| 除上述外的其他通知事件 | `qq.` 前缀加上原名称 | |
|
||||
|
||||
- `friend_recall` 转换后,仅保留 `message_id` 和 `user_id` 字段。
|
||||
- `friend_add` 转换后,仅保留 `user_id` 字段。
|
||||
- `group_increase` 转换后,如果 `sub_type` 值为 `approve` 或空,则转换为 `join`;如果为 `invite` 则不变,如果是其他,则加上前缀 `qq.`。
|
||||
- `group_increase` 转换后,仅保留 `sub_type`、`group_id`、`user_id``operator_id` 且转换为字符串值。
|
||||
- `group_decrease` 转换后,如果 `sub_type` 值为 `kick` 或 `kick_me`,则转换为 `kick`;如果为 `leave` 则不变,如果是其他,则加上前缀 `qq.`。如果想判断是否为 `kick_me`,可以使用判断 `user_id` 和 `operator_id` 是否相同。
|
||||
- `group_decrease` 转换后,仅保留 `sub_type`、`group_id`、`user_id``operator_id` 且转换为字符串值。
|
||||
- `group_recall` 转换后,如果 `user_id` 与 `operator_id` 相同,则 `sub_type` 值设定为 `recall`,否则为 `delete`(分别代表自己撤回和被撤回)。
|
||||
- `group_recall` 转换后,仅保留 `message_id`、`group_id`、`user_id`、`operator_id` 且转换为字符串值。
|
||||
- 其他通知类事件加前缀,其他字段除 `post_type`、`notice_type`、`sub_type`、`request_type`、`meta_event_type`、`time` 和一系列 `xxx_id` 外,均保留,并加上 `qq.` 前缀,值不变。
|
||||
16
composer.json
Normal file
16
composer.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "zhamao/go-cqhttp-adapter-plugin",
|
||||
"license": "Apache-2.0",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"GocqAdapter\\": "src/"
|
||||
}
|
||||
},
|
||||
"require": {
|
||||
"php": "~8.0 || ~8.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"zhamao/framework": "dev-main"
|
||||
},
|
||||
"minimum-stability": "dev"
|
||||
}
|
||||
12
main.php
Normal file
12
main.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
use ZM\Annotation\AnnotationParser;
|
||||
use ZM\Plugin\ZMPlugin;
|
||||
|
||||
$zm = new ZMPlugin(__DIR__);
|
||||
|
||||
$zm->onPluginLoad(function (AnnotationParser $parser) {
|
||||
|
||||
});
|
||||
|
||||
return $zm;
|
||||
58
src/GoBotContext.php
Normal file
58
src/GoBotContext.php
Normal file
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace GocqAdapter;
|
||||
|
||||
use OneBot\Driver\Coroutine\Adaptive;
|
||||
use OneBot\Driver\Event\WebSocket\WebSocketMessageEvent;
|
||||
use OneBot\V12\Object\Action;
|
||||
use OneBot\V12\Object\ActionResponse;
|
||||
use ZM\Annotation\AnnotationHandler;
|
||||
use ZM\Annotation\OneBot\BotAction;
|
||||
use ZM\Context\BotContext;
|
||||
use ZM\Exception\OneBot12Exception;
|
||||
|
||||
class GoBotContext extends BotContext
|
||||
{
|
||||
public function sendAction(string $action, array $params = [], ?array $self = null): bool|ActionResponse
|
||||
{
|
||||
// 前面这里和 OneBot 12 的 sendAction 完全一样
|
||||
// 声明 Action 对象
|
||||
$a = new Action($action, $params, ob_uuidgen(), $self);
|
||||
// 调用事件在回复之前的回调
|
||||
$handler = new AnnotationHandler(BotAction::class);
|
||||
container()->set(Action::class, $a);
|
||||
$handler->setRuleCallback(fn (BotAction $act) => $act->action === '' || $act->action === $action && !$act->need_response);
|
||||
$handler->handleAll($a);
|
||||
// 被阻断时候,就不发送了
|
||||
if ($handler->getStatus() === AnnotationHandler::STATUS_INTERRUPTED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 从这里开始,gocq 需要做一个 12 -> 11 的转换
|
||||
$action_array = GocqActionConverter::getInstance()->convertAction12To11($a);
|
||||
// 将这个 action 提取出来需要记忆的 echo
|
||||
GocqAdapter::$action_hold_list[$a->echo] = $action_array;
|
||||
|
||||
// 调用机器人连接发送 Action
|
||||
if ($this->base_event instanceof WebSocketMessageEvent) {
|
||||
$result = $this->base_event->send(json_encode($action_array));
|
||||
}
|
||||
if (!isset($result) && container()->has('ws.message.event')) {
|
||||
$result = container()->get('ws.message.event')->send(json_encode($action_array));
|
||||
}
|
||||
// 如果开启了协程,并且成功发送,那就进入协程等待,挂起等待结果返回一个 ActionResponse 对象
|
||||
if (($result ?? false) === true && ($co = Adaptive::getCoroutine()) !== null) {
|
||||
static::$coroutine_list[$a->echo] = $co->getCid();
|
||||
$response = $co->suspend();
|
||||
if ($response instanceof ActionResponse) {
|
||||
return $response;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (isset($result)) {
|
||||
return $result;
|
||||
}
|
||||
// 到这里表明你调用时候不在 WS 或 HTTP 上下文
|
||||
throw new OneBot12Exception('No bot connection found.');
|
||||
}
|
||||
}
|
||||
202
src/GocqActionConverter.php
Normal file
202
src/GocqActionConverter.php
Normal file
@ -0,0 +1,202 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GocqAdapter;
|
||||
|
||||
use OneBot\Util\Singleton;
|
||||
use OneBot\V12\Object\Action;
|
||||
use OneBot\V12\Object\ActionResponse;
|
||||
use OneBot\V12\Object\MessageSegment;
|
||||
use ZM\Exception\OneBot12Exception;
|
||||
|
||||
class GocqActionConverter
|
||||
{
|
||||
use Singleton;
|
||||
|
||||
/**
|
||||
* 将 Action 转换为 OneBot 11 的 API 数组
|
||||
*
|
||||
* @param Action $action OneBot 12 的 Action 对象
|
||||
* @return array OneBot 11 的 API 数组
|
||||
* @throws OneBot12Exception
|
||||
*/
|
||||
public function convertAction12To11(Action $action): array
|
||||
{
|
||||
$act = '';
|
||||
$params = [];
|
||||
$echo = $action->echo;
|
||||
switch ($action->action) {
|
||||
case 'send_message':
|
||||
switch ($action->params['detail_type'] ?? '') {
|
||||
case 'group':
|
||||
$act = 'send_group_msg';
|
||||
$params['group_id'] = $action->params['group_id'];
|
||||
$params['message'] = $this->parseSegments12To11($action->params['message']);
|
||||
break;
|
||||
case 'private':
|
||||
$act = 'send_private_msg';
|
||||
$params['user_id'] = $action->params['user_id'];
|
||||
if (isset($action->params['group_id'])) {
|
||||
$params['group_id'] = $action->params['group_id'];
|
||||
}
|
||||
$params['message'] = $this->parseSegments12To11($action->params['message']);
|
||||
break;
|
||||
default:
|
||||
throw new OneBot12Exception('Unsupported detail_type for gocq action');
|
||||
}
|
||||
break;
|
||||
case 'delete_message':
|
||||
$act = 'delete_msg';
|
||||
$params['message_id'] = $action->params['message_id'];
|
||||
break;
|
||||
case 'get_self_info':
|
||||
$act = 'get_login_info';
|
||||
break;
|
||||
case 'get_user_info':
|
||||
$act = 'get_stranger_info';
|
||||
$params['user_id'] = $action->params['user_id'];
|
||||
break;
|
||||
case 'get_friend_list':
|
||||
case 'get_group_info':
|
||||
case 'get_group_list':
|
||||
case 'get_group_member_info':
|
||||
case 'get_group_member_list':
|
||||
case 'set_group_name':
|
||||
$act = $action->action;
|
||||
$params = $action->params;
|
||||
break;
|
||||
case 'leave_group':
|
||||
$act = 'set_group_leave';
|
||||
$params = $action->params;
|
||||
break;
|
||||
case 'upload_file':
|
||||
// OneBot 11 / go-cqhttp 只支持 url 方式上传文件
|
||||
if ($action->params['type'] !== 'url') {
|
||||
throw new OneBot12Exception('OneBot 11 / go-cqhttp only support url uploader');
|
||||
}
|
||||
$act = 'download_file';
|
||||
$params = [
|
||||
'url' => $action->params['url'],
|
||||
'headers' => $action->params['headers'] ?? [],
|
||||
'thread_count' => $action->params['thread_count'] ?? 1,
|
||||
];
|
||||
break;
|
||||
default:
|
||||
// qq. 开头的动作,一律当作 gocq 的其他事件,这时候 params 原封不动发出
|
||||
if (str_starts_with($action->action, 'qq.')) {
|
||||
$act = substr($action->action, 3);
|
||||
$params = $action->params;
|
||||
} else {
|
||||
throw new OneBot12Exception('Current action cannot send to gocq');
|
||||
}
|
||||
break;
|
||||
}
|
||||
return ['action' => $act, 'params' => $params, 'echo' => $echo];
|
||||
}
|
||||
|
||||
public function convertActionResponse11To12(array $response, array $action): ActionResponse
|
||||
{
|
||||
$response_obj = new ActionResponse();
|
||||
$response_obj->status = $response['status'];
|
||||
$response_obj->retcode = GocqRetcodeConverter::getInstance()->convertRetCode11To12($response['retcode']);
|
||||
$response_obj->message = $response['msg'] ?? '';
|
||||
$response_obj->echo = $response['echo'] ?? null;
|
||||
// 接下来判断 action
|
||||
if ($response_obj->retcode !== 0) {
|
||||
return $response_obj;
|
||||
}
|
||||
switch ($action['action']) {
|
||||
case 'send_group_msg':
|
||||
case 'send_private_msg':
|
||||
$response_obj->data = [
|
||||
'message_id' => $response['data']['message_id'],
|
||||
];
|
||||
break;
|
||||
case 'get_stranger_info':
|
||||
$response_obj->data = [
|
||||
'user_id' => $response['data']['user_id'],
|
||||
'user_name' => $response['data']['nickname'],
|
||||
'user_displayname' => $response['data']['nickname'],
|
||||
'user_remark' => '',
|
||||
];
|
||||
break;
|
||||
case 'get_login_info':
|
||||
$response_obj->data = [
|
||||
'user_id' => $response['data']['user_id'],
|
||||
'user_name' => $response['data']['nickname'],
|
||||
'user_displayname' => $response['data']['nickname'],
|
||||
];
|
||||
break;
|
||||
case 'get_friend_list':
|
||||
$response_obj->data = [];
|
||||
foreach ($response['data'] as $friend) {
|
||||
$response_obj->data[] = [
|
||||
'user_id' => $friend['user_id'],
|
||||
'user_name' => $friend['nickname'],
|
||||
'user_displayname' => $friend['nickname'],
|
||||
'user_remark' => $friend['remark'],
|
||||
];
|
||||
}
|
||||
break;
|
||||
case 'get_group_info':
|
||||
case 'get_group_list':
|
||||
$response_obj->data = $response['data'];
|
||||
break;
|
||||
case 'get_group_member_info':
|
||||
$response_obj->data = [
|
||||
'user_id' => $response['data']['user_id'],
|
||||
'user_name' => $response['data']['nickname'],
|
||||
'user_displayname' => $response['data']['card'] ?? $response['data']['nickname'],
|
||||
];
|
||||
foreach ($response['data'] as $kss => $vss) {
|
||||
if (in_array($kss, ['user_id', 'group_id'])) {
|
||||
continue;
|
||||
}
|
||||
$response_obj->data['qq.' . $kss] = $vss;
|
||||
}
|
||||
break;
|
||||
case 'get_group_member_list':
|
||||
foreach ($response['data'] as $member) {
|
||||
$dt = [
|
||||
'user_id' => $member['user_id'],
|
||||
'user_name' => $member['nickname'],
|
||||
'user_displayname' => $member['card'] ?? $member['nickname'],
|
||||
];
|
||||
foreach ($member as $kss => $vss) {
|
||||
if (in_array($kss, ['user_id', 'group_id'])) {
|
||||
continue;
|
||||
}
|
||||
$dt['qq.' . $kss] = $vss;
|
||||
}
|
||||
$response_obj->data[] = $dt;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'download_file':
|
||||
$response_obj->data = [
|
||||
'file_id' => $response['data']['file'],
|
||||
];
|
||||
break;
|
||||
case 'set_group_name':
|
||||
case 'set_group_leave':
|
||||
default:
|
||||
$response_obj->data = $response['data'] ?? [];
|
||||
break;
|
||||
}
|
||||
return $response_obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param MessageSegment[] $message 消息段
|
||||
* @return array OneBot 11 的消息段
|
||||
*/
|
||||
private function parseSegments12To11(array $message): array
|
||||
{
|
||||
$msgs = [];
|
||||
foreach ($message as $v) {
|
||||
$msgs[] = GocqSegmentConverter::getInstance()->parseSegment12To11($v);
|
||||
}
|
||||
return $msgs;
|
||||
}
|
||||
}
|
||||
156
src/GocqAdapter.php
Normal file
156
src/GocqAdapter.php
Normal file
@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GocqAdapter;
|
||||
|
||||
use OneBot\Driver\Event\WebSocket\WebSocketCloseEvent;
|
||||
use OneBot\Driver\Event\WebSocket\WebSocketMessageEvent;
|
||||
use OneBot\Driver\Event\WebSocket\WebSocketOpenEvent;
|
||||
use OneBot\V12\Exception\OneBotException;
|
||||
use OneBot\V12\Object\MessageSegment;
|
||||
use OneBot\V12\Object\OneBotEvent;
|
||||
use ZM\Annotation\AnnotationHandler;
|
||||
use ZM\Annotation\Framework\BindEvent;
|
||||
use ZM\Annotation\Framework\Init;
|
||||
use ZM\Annotation\OneBot\BotActionResponse;
|
||||
use ZM\Annotation\OneBot\BotEvent;
|
||||
use ZM\Annotation\OneBot\CommandArgument;
|
||||
use ZM\Container\ContainerRegistrant;
|
||||
use ZM\Context\BotContext;
|
||||
use ZM\Exception\WaitTimeoutException;
|
||||
use ZM\Utils\ConnectionUtil;
|
||||
|
||||
class GocqAdapter
|
||||
{
|
||||
/**
|
||||
* @var array<string, array>
|
||||
* @internal
|
||||
*/
|
||||
public static array $action_hold_list = [];
|
||||
|
||||
/** @var GocqEventConverter[] */
|
||||
private static array $converters = [];
|
||||
|
||||
#[Init]
|
||||
public function init(): void
|
||||
{
|
||||
logger()->info('go-cqhttp 转换器已加载!');
|
||||
}
|
||||
|
||||
/**
|
||||
* [CALLBACK] 接入和认证 go-cqhttp 的反向 WebSocket 连接
|
||||
* @throws \JsonException
|
||||
*/
|
||||
#[BindEvent(WebSocketOpenEvent::class)]
|
||||
public function handleWSReverseOpen(WebSocketOpenEvent $event): void
|
||||
{
|
||||
logger()->info('连接到 ob11');
|
||||
$request = $event->getRequest();
|
||||
ob_dump($request);
|
||||
// 判断是不是 Gocq 或 OneBot 11 标准的连接。OB11 标准必须带有 X-Client-Role 和 X-Self-ID 两个头。
|
||||
if ($request->getHeaderLine('X-Client-Role') === 'Universal' && $request->getHeaderLine('X-Self-ID') !== '') {
|
||||
logger()->info('检测到 OneBot 11 反向 WS 连接 ' . $request->getHeaderLine('User-Agent'));
|
||||
$info = ['gocq_impl' => 'go-cqhttp', 'self_id' => $request->getHeaderLine('X-Self-ID')];
|
||||
// TODO: 验证 Token
|
||||
ConnectionUtil::setConnection($event->getFd(), $info);
|
||||
logger()->info('已接入 go-cqhttp 的反向 WS 连接,连接 ID 为 ' . $event->getFd());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws OneBotException
|
||||
*/
|
||||
#[BindEvent(WebSocketMessageEvent::class)]
|
||||
public function handleWSReverseMessage(WebSocketMessageEvent $event): void
|
||||
{
|
||||
// 忽略非 gocq 的消息
|
||||
$impl = ConnectionUtil::getConnection($event->getFd())['gocq_impl'] ?? null;
|
||||
if ($impl === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 解析 Frame 到 UTF-8 JSON
|
||||
$body = $event->getFrame()->getData();
|
||||
$body = json_decode($body, true);
|
||||
if ($body === null) {
|
||||
logger()->warning('收到非 JSON 格式的消息,已忽略');
|
||||
return;
|
||||
}
|
||||
|
||||
if (isset($body['post_type'], $body['self_id'])) {
|
||||
$ob12 = self::getConverter($event->getFd(), strval($body['self_id']))->convertEvent($body);
|
||||
if ($ob12 === null) {
|
||||
logger()->debug('收到了不支持的 Event,丢弃此事件');
|
||||
logger()->debug('事件详情对象:' . json_encode($ob12, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
$obj = new OneBotEvent($ob12);
|
||||
} catch (OneBotException $e) {
|
||||
logger()->debug('收到非 OneBot 12(由11转换而来)标准的消息,已忽略');
|
||||
logger()->debug($e->getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
// 绑定容器
|
||||
ContainerRegistrant::registerOBEventServices($obj, GoBotContext::class);
|
||||
|
||||
// 调用 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);
|
||||
});
|
||||
try {
|
||||
$handler->handleAll($obj);
|
||||
} catch (WaitTimeoutException $e) {
|
||||
// 这里是处理 prompt() 下超时的情况的
|
||||
if ($e->getTimeoutPrompt() === null) {
|
||||
return;
|
||||
}
|
||||
if (($e->getPromptOption() & ZM_PROMPT_TIMEOUT_MENTION_USER) === ZM_PROMPT_TIMEOUT_MENTION_USER && ($ev = $e->getUserEvent()) !== null) {
|
||||
$prompt = [MessageSegment::mention($ev->getUserId()), ...$e->getTimeoutPrompt()];
|
||||
}
|
||||
if (($e->getPromptOption() & ZM_PROMPT_TIMEOUT_QUOTE_SELF) === ZM_PROMPT_TIMEOUT_QUOTE_SELF && ($rsp = $e->getPromptResponse()) !== null && ($ev = $e->getUserEvent()) !== null) {
|
||||
$prompt = [MessageSegment::reply($rsp->data['message_id'], $ev->self['user_id']), ...$e->getTimeoutPrompt()];
|
||||
} elseif (($e->getPromptOption() & ZM_PROMPT_TIMEOUT_QUOTE_USER) === ZM_PROMPT_TIMEOUT_QUOTE_USER && ($ev = $e->getUserEvent()) !== null) {
|
||||
$prompt = [MessageSegment::reply($ev->getMessageId(), $ev->getUserId()), ...$e->getTimeoutPrompt()];
|
||||
}
|
||||
bot()->reply($prompt ?? $e->getTimeoutPrompt());
|
||||
}
|
||||
} elseif (isset($body['status'], $body['retcode'], $body['echo'])) {
|
||||
if (isset(self::$action_hold_list[$body['echo']])) {
|
||||
$origin_action = self::$action_hold_list[$body['echo']];
|
||||
unset(self::$action_hold_list[$body['echo']]);
|
||||
$resp = GocqActionConverter::getInstance()->convertActionResponse11To12($body, $origin_action);
|
||||
ContainerRegistrant::registerOBActionResponseServices($resp);
|
||||
|
||||
// 调用 BotActionResponse 事件
|
||||
$handler = new AnnotationHandler(BotActionResponse::class);
|
||||
$handler->setRuleCallback(function (BotActionResponse $event) use ($resp) {
|
||||
return $event->retcode === null || $event->retcode === $resp->retcode;
|
||||
});
|
||||
$handler->handleAll($resp);
|
||||
|
||||
// 如果有协程,并且该 echo 记录在案的话,就恢复协程
|
||||
BotContext::tryResume($resp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[BindEvent(WebSocketCloseEvent::class)]
|
||||
public function handleWSReverseClose(WebSocketCloseEvent $event): void
|
||||
{
|
||||
unset(self::$converters[$event->getFd()]);
|
||||
}
|
||||
|
||||
public static function getConverter(int $fd, ?string $self_id = null): GocqEventConverter
|
||||
{
|
||||
if (!isset(self::$converters[$fd])) {
|
||||
self::$converters[$fd] = new GocqEventConverter($self_id, 'unknown');
|
||||
}
|
||||
return self::$converters[$fd];
|
||||
}
|
||||
}
|
||||
236
src/GocqEventConverter.php
Normal file
236
src/GocqEventConverter.php
Normal file
@ -0,0 +1,236 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GocqAdapter;
|
||||
|
||||
use OneBot\V12\Object\MessageSegment;
|
||||
|
||||
class GocqEventConverter
|
||||
{
|
||||
public function __construct(private string $self_id, private string $version)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 OneBot11/go-cqhttp 的事件转换为 OneBot 12 可用的事件
|
||||
*
|
||||
* 其中有几个细节:
|
||||
* 1. self 下的 platform 直接使用 qq 代替,未来说不定再添加动态的
|
||||
* 2. message_sent 直接默认转换,如果不需要 message_sent 事件请在 gocq 的配置中取消
|
||||
* 3. 事件的 id 为框架生成的随机 ID,并非 gocq 生成
|
||||
* 4. 仅将 gocq 写入文档的非标准内的事件字段转换为扩展字段
|
||||
*
|
||||
* @param array $event OneBot11/go-cqhttp 事件原数据
|
||||
*/
|
||||
public function convertEvent(array $event): ?array
|
||||
{
|
||||
$ob12 = [];
|
||||
// post_type 转换为 type
|
||||
$ob12['type'] = $event['post_type'];
|
||||
// self_id 转换为 self
|
||||
if (isset($event['self_id'])) {
|
||||
$ob12['self'] = [
|
||||
'user_id' => strval($event['self_id']),
|
||||
'platform' => 'qq',
|
||||
];
|
||||
} elseif ($event['post_type'] !== 'meta_event') {
|
||||
$ob12['self'] = [
|
||||
'user_id' => $this->self_id,
|
||||
'platform' => 'qq',
|
||||
];
|
||||
}
|
||||
// time 原封不动
|
||||
$ob12['time'] = $event['time'];
|
||||
// 生成一个事件 ID
|
||||
$ob12['id'] = ob_uuidgen();
|
||||
switch ($ob12['type']) {
|
||||
case 'message':
|
||||
case 'message_sent':
|
||||
// message_type 转换为 detail_type
|
||||
$ob12['detail_type'] = $event['message_type'];
|
||||
// TODO:目前只适配了 gocq 的 private 和 group 类型,以后再适配 guild,因为 gocq 的 guild 太特殊了
|
||||
if (!in_array($ob12['detail_type'], ['private', 'group'])) {
|
||||
return null;
|
||||
}
|
||||
// sub_type 原封不动
|
||||
$ob12['sub_type'] = $event['sub_type'];
|
||||
// message_id 需要 strval 后
|
||||
$ob12['message_id'] = strval($event['message_id']);
|
||||
// user_id 需要 strval
|
||||
$ob12['user_id'] = strval($event['user_id']);
|
||||
// 转换下消息
|
||||
$ob12['message'] = $this->convertMessageSegment($event['message']);
|
||||
// raw_message 转换为 alt_message
|
||||
$ob12['alt_message'] = $event['raw_message'];
|
||||
// sender 转换为 qq.sender
|
||||
$ob12['qq.sender'] = $event['sender'];
|
||||
// font 转换为 qq.font
|
||||
$ob12['qq.font'] = $event['font'];
|
||||
// message_type 为 group 时,需要转换 group_id
|
||||
if ($ob12['detail_type'] === 'group') {
|
||||
$ob12['group_id'] = strval($event['group_id']);
|
||||
}
|
||||
break;
|
||||
case 'notice':
|
||||
$ob12 = $this->convertNoticeEvent($ob12, $event);
|
||||
break;
|
||||
case 'request':
|
||||
$ob12 = $this->convertRequestEvent($ob12, $event);
|
||||
break;
|
||||
case 'meta_event':
|
||||
$ob12 = $this->convertMetaEvent($ob12, $event);
|
||||
break;
|
||||
}
|
||||
// 有的事件没有 sub_type,补上
|
||||
if (!isset($ob12['sub_type'])) {
|
||||
$ob12['sub_type'] = '';
|
||||
}
|
||||
return $ob12;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 OneBot 11 的消息转换为 OneBot 12 的消息段
|
||||
*/
|
||||
public function convertMessageSegment(string|array $message): array
|
||||
{
|
||||
// 如果是 string,先读 CQ 码
|
||||
if (is_string($message)) {
|
||||
$message = GocqSegmentConverter::getInstance()->strToSegments($message);
|
||||
} else {
|
||||
foreach ($message as $k => $v) {
|
||||
[$type, $data] = GocqSegmentConverter::getInstance()->parseSegment11To12($v['type'], $v['data'] ?? []);
|
||||
$message[$k] = ['type' => $type, 'data' => $data];
|
||||
}
|
||||
}
|
||||
return $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换通知事件
|
||||
*
|
||||
* @param array $ob12 OneBot 12 事件数组
|
||||
* @param array $event OneBot 11 / go-cqhttp 事件数组
|
||||
*/
|
||||
public function convertNoticeEvent(array $ob12, array $event): array
|
||||
{
|
||||
// 标准对照表,将一些特定 OneBot 12 中规定了的事件转换到标准的,剩下的都加上前缀
|
||||
switch ($event['notice_type']) {
|
||||
case 'friend_recall': // 消息撤回,转换为 private_message_delete
|
||||
$ob12['detail_type'] = 'private_message_delete';
|
||||
$ob12['message_id'] = strval($event['message_id']);
|
||||
$ob12['user_id'] = strval($event['user_id']);
|
||||
break;
|
||||
case 'friend_add': // 好友添加,转换为 friend_increase
|
||||
$ob12['detail_type'] = 'friend_increase';
|
||||
$ob12['user_id'] = strval($event['user_id']);
|
||||
break;
|
||||
case 'group_increase': // 群成员增加,转换为 group_member_increase
|
||||
$ob12['detail_type'] = 'group_member_increase';
|
||||
$ob12['sub_type'] = match ($event['sub_type']) {
|
||||
'approve', '' => 'join',
|
||||
'invite' => 'invite',
|
||||
// no break
|
||||
default => 'qq.' . $event['sub_type'],
|
||||
};
|
||||
$ob12['group_id'] = strval($event['group_id']);
|
||||
$ob12['user_id'] = strval($event['user_id']);
|
||||
$ob12['operator_id'] = strval($event['operator_id']);
|
||||
break;
|
||||
case 'group_decrease': // 群成员减少,转换为 group_member_decrease
|
||||
$ob12['detail_type'] = 'group_member_decrease';
|
||||
$ob12['sub_type'] = match ($event['sub_type']) {
|
||||
'leave' => 'leave',
|
||||
'kick', 'kick_me' => 'kick',
|
||||
// no break
|
||||
default => ('qq.' . $event['sub_type']),
|
||||
};
|
||||
$ob12['group_id'] = strval($event['group_id']);
|
||||
$ob12['user_id'] = strval($event['user_id']);
|
||||
$ob12['operator_id'] = strval($event['operator_id']);
|
||||
break;
|
||||
case 'group_recall': // 群消息撤回,转换为 group_message_delete
|
||||
$ob12['detail_type'] = 'group_message_delete';
|
||||
$ob12['sub_type'] = $event['user_id'] == $event['operator_id'] ? 'recall' : 'delete';
|
||||
$ob12['group_id'] = strval($event['group_id']);
|
||||
$ob12['message_id'] = strval($event['message_id']);
|
||||
$ob12['user_id'] = strval($event['user_id']);
|
||||
$ob12['operator_id'] = strval($event['operator_id']);
|
||||
break;
|
||||
default: // 其他的 notice 事件,统一加上前缀
|
||||
$ob12['detail_type'] = 'qq.' . $event['notice_type'];
|
||||
$ob12 = $this->parseExtendedKeys($ob12, $event);
|
||||
break;
|
||||
}
|
||||
return $ob12;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换请求事件
|
||||
*
|
||||
* @param array $ob12 OneBot 12 事件数组
|
||||
* @param array $event OneBot 11 / go-cqhttp 事件数组
|
||||
*/
|
||||
public function convertRequestEvent(array $ob12, array $event): array
|
||||
{
|
||||
// OneBot 12 标准中没有规定任何 request,所以任何 request 都加前缀
|
||||
$ob12['detail_type'] = 'qq.' . $event['request_type'];
|
||||
return $this->parseExtendedKeys($ob12, $event);
|
||||
}
|
||||
|
||||
public function convertMetaEvent(array $ob12, array $event): array
|
||||
{
|
||||
$ob12['type'] = 'meta';
|
||||
switch ($event['meta_event_type']) {
|
||||
case 'lifecycle':
|
||||
if ($event['sub_type'] == 'connect') {
|
||||
$ob12['detail_type'] = 'connect';
|
||||
$ob12['version'] = [
|
||||
'impl' => 'go-cqhttp',
|
||||
'version' => $this->version,
|
||||
'onebot_version' => '12',
|
||||
];
|
||||
}
|
||||
break;
|
||||
case 'heartbeat':
|
||||
$ob12['detail_type'] = 'heartbeat';
|
||||
$ob12['interval'] = $event['interval'];
|
||||
break;
|
||||
default:
|
||||
$ob12['detail_type'] = $event['meta_event_type'];
|
||||
$ob12 = $this->parseExtendedKeys($ob12, $event);
|
||||
}
|
||||
$ob12['sub_type'] = $ob12['detail_type'] === 'connect' ? '' : ($event['sub_type'] ?? '');
|
||||
return $ob12;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 OneBot 11 / go-cqhttp 事件数组中的其他字段进行前缀化
|
||||
*
|
||||
* @param array $ob12 OneBot 12 事件数组
|
||||
* @param array $event OneBot 11 / go-cqhttp 事件数组
|
||||
*/
|
||||
public function parseExtendedKeys(array $ob12, array $event): array
|
||||
{
|
||||
foreach ($event as $k => $v) {
|
||||
/*
|
||||
其他事件转换规则:
|
||||
1. 'post_type', 'notice_type', 'request_type', 'meta_event_type', 'time', 'self_id' 这几个字段忽略
|
||||
2. 现有 ID 类,'user_id', 'group_id', 'channel_id', 'guild_id', 'operator_id', 'message_id' 这几个 id 类的需要取字符串值
|
||||
3. 'sub_type' 如果值不为空,则直接加上前缀,否则保持空着
|
||||
4. 其他字段,统一加上前缀,值不变
|
||||
*/
|
||||
$result = match ($k) {
|
||||
'post_type', 'notice_type', 'request_type', 'meta_event_type', 'time', 'self_id' => null,
|
||||
'user_id', 'group_id', 'channel_id', 'guild_id', 'operator_id', 'message_id' => [$k, strval($v)],
|
||||
'sub_type' => $v === '' ? [$k, $v] : ($event['post_type'] === 'meta_event' ? [$k, $v] : [$k, 'qq.' . $v]),
|
||||
default => ['qq.' . $k, $v],
|
||||
};
|
||||
if ($result !== null) {
|
||||
$ob12[$result[0]] = $result[1];
|
||||
}
|
||||
}
|
||||
return $ob12;
|
||||
}
|
||||
|
||||
}
|
||||
15
src/GocqRetcodeConverter.php
Normal file
15
src/GocqRetcodeConverter.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace GocqAdapter;
|
||||
|
||||
use OneBot\Util\Singleton;
|
||||
|
||||
class GocqRetcodeConverter
|
||||
{
|
||||
use Singleton;
|
||||
|
||||
public function convertRetCode11To12(int $retcode): int
|
||||
{
|
||||
return $retcode !== 0 ? 10000 : 0;
|
||||
}
|
||||
}
|
||||
172
src/GocqSegmentConverter.php
Normal file
172
src/GocqSegmentConverter.php
Normal file
@ -0,0 +1,172 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GocqAdapter;
|
||||
|
||||
use OneBot\Util\Singleton;
|
||||
use OneBot\V12\Object\MessageSegment;
|
||||
|
||||
class GocqSegmentConverter
|
||||
{
|
||||
use Singleton;
|
||||
|
||||
/**
|
||||
* 将字符串转换为消息段格式,并且按照 11 -> 12 进行兼容性转换
|
||||
*
|
||||
* @param string $msg 带 CQ 码的字符串
|
||||
*/
|
||||
public function strToSegments(string $msg): array
|
||||
{
|
||||
$segments = [];
|
||||
// 循环找 CQ 码
|
||||
while ($msg !== '') {
|
||||
$before = mb_strstr($msg, '[CQ:', true);
|
||||
$after = mb_strstr($msg, '[CQ:');
|
||||
// 找不到 CQ 码,直接返回原文本
|
||||
if ($before === false) {
|
||||
$segments[] = MessageSegment::text($msg);
|
||||
break;
|
||||
}
|
||||
// 找下 ],没找到的话返回消息
|
||||
if (($close = mb_strpos($after, ']')) === false) {
|
||||
$segments[] = MessageSegment::text($msg);
|
||||
break;
|
||||
}
|
||||
// 这里拿到右括号了,我们读取左括号和右括号之间的内容进行解析
|
||||
$cq = mb_substr($after, 4, $close - 4);
|
||||
// 读取到的 CQ 码 type
|
||||
$cqs = explode(',', $cq);
|
||||
$type = array_shift($cqs);
|
||||
// 剩下的都是参数,我们用等于号再次分割可以获得kv
|
||||
$params = [];
|
||||
foreach ($cqs as $v) {
|
||||
$kv = explode('=', $v);
|
||||
$key = array_shift($kv);
|
||||
$params[$key] = $this->cqDecode(implode('=', $kv));
|
||||
}
|
||||
// 将 11 的消息段转换为 12 的消息段
|
||||
[$type, $params] = $this->parseSegment11To12($type, $params);
|
||||
// CQ 码前面有文本,当作一个消息段
|
||||
if ($before !== '') {
|
||||
$segments[] = MessageSegment::text($before);
|
||||
}
|
||||
$segments[] = new MessageSegment($type, $params);
|
||||
$msg = mb_substr($after, $close + 1);
|
||||
}
|
||||
return $segments;
|
||||
}
|
||||
|
||||
/**
|
||||
* 反转义 CQ 码特殊字符(仅参数内容)
|
||||
*
|
||||
* @param string $content 内容
|
||||
*/
|
||||
public function cqDecode(string $content): string
|
||||
{
|
||||
return str_replace(['&', '[', ']', ','], ['&', '[', ']', ','], $content);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 OneBot 11 消息段转换为 OneBot 12 消息段
|
||||
*
|
||||
* @param string $type 类型
|
||||
* @param array $data 参数
|
||||
*/
|
||||
public function parseSegment11To12(string $type, array $data): array
|
||||
{
|
||||
switch ($type) {
|
||||
case 'at':
|
||||
$qq = $data['qq'];
|
||||
unset($data['qq']);
|
||||
if ($qq === 'all') {
|
||||
$type = 'mention_all';
|
||||
} else {
|
||||
$type = 'mention';
|
||||
$data['user_id'] = $qq;
|
||||
}
|
||||
break;
|
||||
case 'video':
|
||||
case 'image':
|
||||
// 使用 file 字段当作 file_id
|
||||
$file = $data['file'];
|
||||
unset($data['file']);
|
||||
$data['file_id'] = $file;
|
||||
break;
|
||||
case 'record':
|
||||
$type = 'voice';
|
||||
// 使用 file 字段当作 file_id
|
||||
$file = $data['file'];
|
||||
unset($data['file']);
|
||||
$data['file_id'] = $file;
|
||||
break;
|
||||
case 'location':
|
||||
$data_old = $data;
|
||||
$data = [
|
||||
'latitude' => floatval($data_old['lat']),
|
||||
'longitude' => floatval($data_old['lon']),
|
||||
'title' => $data_old['title'] ?? '',
|
||||
'content' => $data_old['content'] ?? '',
|
||||
];
|
||||
break;
|
||||
case 'reply':
|
||||
$id = $data['id'];
|
||||
unset($data['id']);
|
||||
$data['message_id'] = $id;
|
||||
if (isset($data['qq'])) {
|
||||
$qq = $data['qq'];
|
||||
unset($data['qq']);
|
||||
$data['user_id'] = strval($qq);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
$type = 'qq.' . $type;
|
||||
break;
|
||||
}
|
||||
return [$type, $data];
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 OneBot 消息段由 12 -> 11
|
||||
* @param MessageSegment $segment OneBot 12 的消息段
|
||||
* @return array OneBot 11 的消息段
|
||||
*/
|
||||
public function parseSegment12To11(MessageSegment $segment): array
|
||||
{
|
||||
switch ($segment->type) {
|
||||
case 'text':
|
||||
return ['type' => 'text', 'data' => ['text' => $segment->data['text']]];
|
||||
case 'mention':
|
||||
return ['type' => 'at', 'data' => ['qq' => $segment->data['user_id']]];
|
||||
case 'mention_all':
|
||||
return ['type' => 'at', 'data' => ['qq' => 'all']];
|
||||
case 'image':
|
||||
return ['type' => 'image', 'data' => ['file' => $segment->data['file_id'], 'url' => $segment->data['url'] ?? '']];
|
||||
case 'voice':
|
||||
return ['type' => 'record', 'data' => ['file' => $segment->data['file_id'], 'url' => $segment->data['url'] ?? '']];
|
||||
case 'video':
|
||||
return ['type' => 'video', 'data' => ['file' => $segment->data['file_id'], 'url' => $segment->data['url'] ?? '']];
|
||||
case 'location':
|
||||
return ['type' => 'location', 'data' => [
|
||||
'lat' => $segment->data['latitude'],
|
||||
'lon' => $segment->data['longitude'],
|
||||
'title' => $segment->data['title'],
|
||||
'content' => $segment->data['content'],
|
||||
]];
|
||||
case 'reply':
|
||||
$data = ['id' => $segment->data['message_id']];
|
||||
if (isset($segment->data['user_id'])) {
|
||||
$data['qq'] = $segment->data['user_id'];
|
||||
}
|
||||
return ['type' => 'reply', 'data' => $data];
|
||||
default:
|
||||
if (str_starts_with($segment->type, 'qq.')) {
|
||||
$type = substr($segment->type, 3);
|
||||
} else {
|
||||
$type = $segment->type;
|
||||
}
|
||||
$data = $segment->data;
|
||||
return ['type' => $type, 'data' => $data];
|
||||
}
|
||||
}
|
||||
}
|
||||
5
zmplugin.json
Normal file
5
zmplugin.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "go-cqhttp-adapter-plugin",
|
||||
"version": "1.0.0",
|
||||
"main": "main.php"
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user