@alemonjs/qq-bot 2.1.0-alpha.17 → 2.1.0-alpha.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -30
- package/lib/desktop.js +3 -2
- package/lib/hook.js +6 -6
- package/lib/index.d.ts +2 -2
- package/lib/index.js +33 -4
- package/lib/index.webhook.js +1 -1
- package/lib/index.websoket.js +3 -3
- package/lib/register.js +68 -36
- package/lib/sdk/api.d.ts +14 -14
- package/lib/sdk/api.js +142 -140
- package/lib/sdk/client.webhook.js +19 -14
- package/lib/sdk/client.websoket.js +38 -38
- package/lib/sdk/instance.js +109 -0
- package/lib/sdk/webhook.secret.js +5 -3
- package/lib/sends.js +66 -71
- package/package.json +1 -1
|
@@ -43,16 +43,18 @@ class QQBotClient extends QQBotAPI {
|
|
|
43
43
|
async #setTimeoutBotConfig() {
|
|
44
44
|
const callBack = async () => {
|
|
45
45
|
const app_id = config.get('app_id');
|
|
46
|
-
if (!app_id)
|
|
46
|
+
if (!app_id) {
|
|
47
47
|
return;
|
|
48
|
+
}
|
|
48
49
|
const secret = config.get('secret');
|
|
49
|
-
if (!secret)
|
|
50
|
+
if (!secret) {
|
|
50
51
|
return;
|
|
52
|
+
}
|
|
51
53
|
// 发送请求
|
|
52
54
|
const data = await this.getAuthentication(app_id, secret).then(res => res.data);
|
|
53
55
|
config.set('access_token', data.access_token);
|
|
54
56
|
console.info('refresh', data.expires_in, 's');
|
|
55
|
-
setTimeout(callBack, data.expires_in * 1000);
|
|
57
|
+
setTimeout(() => void callBack(), data.expires_in * 1000);
|
|
56
58
|
};
|
|
57
59
|
await callBack();
|
|
58
60
|
}
|
|
@@ -65,7 +67,7 @@ class QQBotClient extends QQBotAPI {
|
|
|
65
67
|
try {
|
|
66
68
|
const ws = config.get('ws');
|
|
67
69
|
if (!ws) {
|
|
68
|
-
this.#setTimeoutBotConfig();
|
|
70
|
+
void this.#setTimeoutBotConfig();
|
|
69
71
|
this.#app = new Koa();
|
|
70
72
|
this.#app.use(bodyParser());
|
|
71
73
|
const router = new Router();
|
|
@@ -86,7 +88,7 @@ class QQBotClient extends QQBotAPI {
|
|
|
86
88
|
await next();
|
|
87
89
|
});
|
|
88
90
|
// 启动服务
|
|
89
|
-
router.post(route,
|
|
91
|
+
router.post(route, ctx => {
|
|
90
92
|
const sign = ctx.req.headers['x-signature-ed25519'];
|
|
91
93
|
const timestamp = ctx.req.headers['x-signature-timestamp'];
|
|
92
94
|
const rawBody = ctx.request.rawBody;
|
|
@@ -97,7 +99,7 @@ class QQBotClient extends QQBotAPI {
|
|
|
97
99
|
return;
|
|
98
100
|
}
|
|
99
101
|
const body = ctx.request.body;
|
|
100
|
-
if (body.op
|
|
102
|
+
if (+body.op === 13) {
|
|
101
103
|
ctx.status = 200;
|
|
102
104
|
ctx.body = {
|
|
103
105
|
// 返回明文 token
|
|
@@ -106,18 +108,19 @@ class QQBotClient extends QQBotAPI {
|
|
|
106
108
|
signature: ntqqWebhook.getSign(body.d.event_ts, body.d.plain_token)
|
|
107
109
|
};
|
|
108
110
|
}
|
|
109
|
-
else if (body.op
|
|
111
|
+
else if (+body.op === 0) {
|
|
110
112
|
ctx.status = 204;
|
|
111
113
|
// 根据事件类型,处理事件
|
|
112
114
|
for (const event of this.#events[body.t] || []) {
|
|
113
115
|
event(body.d);
|
|
114
116
|
}
|
|
115
|
-
const
|
|
117
|
+
const accessToken = config.get('access_token');
|
|
116
118
|
// 也可以分法到客户端。 发送失败需要处理 或清理调
|
|
117
119
|
for (const client of this.#client) {
|
|
118
120
|
try {
|
|
119
|
-
if (
|
|
120
|
-
body['access_token'] =
|
|
121
|
+
if (accessToken) {
|
|
122
|
+
body['access_token'] = accessToken;
|
|
123
|
+
}
|
|
121
124
|
client.ws.send(JSON.stringify(body));
|
|
122
125
|
}
|
|
123
126
|
catch (e) {
|
|
@@ -173,9 +176,10 @@ class QQBotClient extends QQBotAPI {
|
|
|
173
176
|
try {
|
|
174
177
|
// 拿到消息
|
|
175
178
|
const body = JSON.parse(data.toString());
|
|
176
|
-
const
|
|
177
|
-
if (
|
|
178
|
-
config.set('access_token',
|
|
179
|
+
const accessToken = body['access_token'];
|
|
180
|
+
if (accessToken) {
|
|
181
|
+
config.set('access_token', accessToken);
|
|
182
|
+
}
|
|
179
183
|
for (const event of this.#events[body.t] || []) {
|
|
180
184
|
event(body.d);
|
|
181
185
|
}
|
|
@@ -187,8 +191,9 @@ class QQBotClient extends QQBotAPI {
|
|
|
187
191
|
this.#ws.on('close', () => {
|
|
188
192
|
console.log('ws closed');
|
|
189
193
|
// 重连5次,超过5次不再重连
|
|
190
|
-
if (this.#count > 5)
|
|
194
|
+
if (this.#count > 5) {
|
|
191
195
|
return;
|
|
196
|
+
}
|
|
192
197
|
// 1.3s 后重连
|
|
193
198
|
setTimeout(() => {
|
|
194
199
|
reConnect();
|
|
@@ -37,16 +37,16 @@ class QQBotClients extends QQBotAPI {
|
|
|
37
37
|
const accessToken = async () => {
|
|
38
38
|
const app_id = config.get('app_id');
|
|
39
39
|
const secret = config.get('secret');
|
|
40
|
-
if (!app_id || !secret)
|
|
40
|
+
if (!app_id || !secret) {
|
|
41
41
|
return;
|
|
42
|
+
}
|
|
42
43
|
// 发送请求
|
|
43
44
|
const data = await this.getAuthentication(app_id, secret).then(res => res.data);
|
|
44
45
|
config.set('access_token', data.access_token);
|
|
45
46
|
console.info('refresh', data.expires_in, 's');
|
|
46
|
-
setTimeout(accessToken, data.expires_in * 1000);
|
|
47
|
+
setTimeout(() => void accessToken(), data.expires_in * 1000);
|
|
47
48
|
};
|
|
48
49
|
await accessToken();
|
|
49
|
-
return;
|
|
50
50
|
}
|
|
51
51
|
/**
|
|
52
52
|
* 鉴权数据
|
|
@@ -78,8 +78,9 @@ class QQBotClients extends QQBotAPI {
|
|
|
78
78
|
* @param val 事件处理函数
|
|
79
79
|
*/
|
|
80
80
|
on(key, val) {
|
|
81
|
-
if (!this.#events[key])
|
|
81
|
+
if (!this.#events[key]) {
|
|
82
82
|
this.#events[key] = [];
|
|
83
|
+
}
|
|
83
84
|
this.#events[key].push(val);
|
|
84
85
|
return this;
|
|
85
86
|
}
|
|
@@ -95,16 +96,17 @@ class QQBotClients extends QQBotAPI {
|
|
|
95
96
|
if (!this.#gatewayUrl) {
|
|
96
97
|
this.#gatewayUrl = gatewayURL ?? (await this.gateway().then(res => res?.url));
|
|
97
98
|
}
|
|
98
|
-
if (!this.#gatewayUrl)
|
|
99
|
+
if (!this.#gatewayUrl) {
|
|
99
100
|
return;
|
|
101
|
+
}
|
|
100
102
|
// 重新连接的逻辑
|
|
101
|
-
const reconnect =
|
|
103
|
+
const reconnect = () => {
|
|
102
104
|
if (this.#counter.value >= 5) {
|
|
103
105
|
console.info('The maximum number of reconnections has been reached, cancel reconnection');
|
|
104
106
|
return;
|
|
105
107
|
}
|
|
106
108
|
setTimeout(() => {
|
|
107
|
-
console.info('[ws] reconnecting...');
|
|
109
|
+
console.info('[ws-qqbot] reconnecting...');
|
|
108
110
|
// 重新starrt
|
|
109
111
|
start();
|
|
110
112
|
// 记录
|
|
@@ -114,13 +116,14 @@ class QQBotClients extends QQBotAPI {
|
|
|
114
116
|
const start = () => {
|
|
115
117
|
if (this.#gatewayUrl) {
|
|
116
118
|
const map = {
|
|
117
|
-
0:
|
|
119
|
+
0: ({ t, d }) => {
|
|
118
120
|
if (this.#events[t]) {
|
|
119
121
|
try {
|
|
120
122
|
for (const event of this.#events[t]) {
|
|
121
123
|
// 是否是函数
|
|
122
|
-
if (typeof event
|
|
124
|
+
if (typeof event !== 'function') {
|
|
123
125
|
continue;
|
|
126
|
+
}
|
|
124
127
|
event(d);
|
|
125
128
|
}
|
|
126
129
|
}
|
|
@@ -128,8 +131,9 @@ class QQBotClients extends QQBotAPI {
|
|
|
128
131
|
if (this.#events['ERROR']) {
|
|
129
132
|
for (const event of this.#events['ERROR']) {
|
|
130
133
|
// 是否是函数
|
|
131
|
-
if (typeof event
|
|
134
|
+
if (typeof event !== 'function') {
|
|
132
135
|
continue;
|
|
136
|
+
}
|
|
133
137
|
event(err);
|
|
134
138
|
}
|
|
135
139
|
}
|
|
@@ -138,38 +142,34 @@ class QQBotClients extends QQBotAPI {
|
|
|
138
142
|
// Ready Event,鉴权成功
|
|
139
143
|
if (t === 'READY') {
|
|
140
144
|
this.#IntervalId = setInterval(() => {
|
|
141
|
-
if (this.#isConnected) {
|
|
142
|
-
this.#ws
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}));
|
|
145
|
+
if (this.#isConnected && this.#ws) {
|
|
146
|
+
this.#ws.send(JSON.stringify({
|
|
147
|
+
op: 1, // op = 1
|
|
148
|
+
d: null // 如果是第一次连接,传null
|
|
149
|
+
}));
|
|
147
150
|
}
|
|
148
151
|
}, this.#heartbeat_interval);
|
|
149
152
|
}
|
|
150
153
|
// Resumed Event,恢复连接成功
|
|
151
154
|
if (t === 'RESUMED') {
|
|
152
|
-
console.info('[ws] restore connection');
|
|
155
|
+
console.info('[ws-qqbot] restore connection');
|
|
153
156
|
// 重制次数
|
|
154
157
|
this.#counter.reStart();
|
|
155
158
|
}
|
|
156
|
-
return;
|
|
157
159
|
},
|
|
158
160
|
6: ({ d }) => {
|
|
159
|
-
console.info('[ws] connection attempt', d);
|
|
160
|
-
return;
|
|
161
|
+
console.info('[ws-qqbot] connection attempt', d);
|
|
161
162
|
},
|
|
162
|
-
7:
|
|
163
|
+
7: ({ d }) => {
|
|
163
164
|
// 执行重新连接
|
|
164
|
-
console.info('[ws] reconnect', d);
|
|
165
|
+
console.info('[ws-qqbot] reconnect', d);
|
|
165
166
|
// 取消鉴权发送
|
|
166
|
-
if (this.#IntervalId)
|
|
167
|
+
if (this.#IntervalId) {
|
|
167
168
|
clearInterval(this.#IntervalId);
|
|
168
|
-
|
|
169
|
+
}
|
|
169
170
|
},
|
|
170
171
|
9: ({ d }) => {
|
|
171
|
-
console.info('[ws] parameter error', d);
|
|
172
|
-
return;
|
|
172
|
+
console.info('[ws-qqbot] parameter error', d);
|
|
173
173
|
},
|
|
174
174
|
10: ({ d }) => {
|
|
175
175
|
// 重制次数
|
|
@@ -177,40 +177,40 @@ class QQBotClients extends QQBotAPI {
|
|
|
177
177
|
// 记录新循环
|
|
178
178
|
this.#heartbeat_interval = d.heartbeat_interval;
|
|
179
179
|
// 发送鉴权
|
|
180
|
-
|
|
181
|
-
|
|
180
|
+
if (this.#ws) {
|
|
181
|
+
this.#ws.send(JSON.stringify(this.#aut()));
|
|
182
|
+
}
|
|
182
183
|
},
|
|
183
184
|
11: () => {
|
|
184
185
|
// OpCode 11 Heartbeat ACK 消息,心跳发送成功
|
|
185
|
-
console.info('[ws] heartbeat transmission');
|
|
186
|
+
console.info('[ws-qqbot] heartbeat transmission');
|
|
186
187
|
// 重制次数
|
|
187
188
|
this.#counter.reStart();
|
|
188
|
-
return;
|
|
189
189
|
},
|
|
190
190
|
12: ({ d }) => {
|
|
191
|
-
console.
|
|
192
|
-
return;
|
|
191
|
+
console.debug('[ws-qqbot] platform data', d);
|
|
193
192
|
}
|
|
194
193
|
};
|
|
195
194
|
// 连接
|
|
196
195
|
this.#ws = new WebSocket(this.#gatewayUrl);
|
|
197
196
|
this.#ws.on('open', () => {
|
|
198
|
-
console.info('[ws] open');
|
|
197
|
+
console.info('[ws-qqbot] open');
|
|
199
198
|
});
|
|
200
199
|
// 监听消息
|
|
201
|
-
this.#ws.on('message',
|
|
200
|
+
this.#ws.on('message', msg => {
|
|
202
201
|
const message = JSON.parse(msg.toString('utf8'));
|
|
203
|
-
if (process.env.NTQQ_WS
|
|
202
|
+
if (process.env.NTQQ_WS === 'dev') {
|
|
204
203
|
console.info('message', message);
|
|
204
|
+
}
|
|
205
205
|
// 根据 opcode 进行处理
|
|
206
206
|
if (map[message.op]) {
|
|
207
207
|
map[message.op](message);
|
|
208
208
|
}
|
|
209
209
|
});
|
|
210
210
|
// 关闭
|
|
211
|
-
this.#ws.on('close',
|
|
212
|
-
|
|
213
|
-
console.info('[ws] close', err);
|
|
211
|
+
this.#ws.on('close', err => {
|
|
212
|
+
void reconnect();
|
|
213
|
+
console.info('[ws-qqbot] close', err);
|
|
214
214
|
});
|
|
215
215
|
}
|
|
216
216
|
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
const filterHeaders = (headers = {}) => {
|
|
2
|
+
if (!headers) {
|
|
3
|
+
return headers;
|
|
4
|
+
}
|
|
5
|
+
const filtered = {};
|
|
6
|
+
const sensitiveKeys = [/^authorization$/i, /^cookie$/i, /^set-cookie$/i, /token/i, /key/i, /jwt/i, /^session[-_]id$/i, /^uid$/i, /^user[-_]id$/i];
|
|
7
|
+
for (const key in headers) {
|
|
8
|
+
if (/^_/.test(key)) {
|
|
9
|
+
continue; // 跳过 _ 开头
|
|
10
|
+
}
|
|
11
|
+
// 去掉 Symbol 类型的 key
|
|
12
|
+
if (typeof key === 'symbol') {
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
// 去掉函数
|
|
16
|
+
if (typeof headers[key] === 'function') {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
// 如果是敏感字段全部替换为 ******
|
|
20
|
+
if (sensitiveKeys.some(re => re.test(key))) {
|
|
21
|
+
filtered[key] = '******';
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
filtered[key] = headers[key];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return filtered;
|
|
28
|
+
};
|
|
29
|
+
const filterConfig = (config = {}) => {
|
|
30
|
+
if (!config) {
|
|
31
|
+
return config;
|
|
32
|
+
}
|
|
33
|
+
const filtered = {};
|
|
34
|
+
for (const key in config) {
|
|
35
|
+
if (/^_/.test(key)) {
|
|
36
|
+
continue; // 跳过 _ 开头
|
|
37
|
+
}
|
|
38
|
+
// 去掉 Symbol 类型的 key
|
|
39
|
+
if (typeof key === 'symbol') {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
// 去掉函数
|
|
43
|
+
if (typeof config[key] === 'function') {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
filtered[key] = config[key];
|
|
47
|
+
}
|
|
48
|
+
return filtered;
|
|
49
|
+
};
|
|
50
|
+
const filterRequest = (request = {}) => {
|
|
51
|
+
if (!request) {
|
|
52
|
+
return request;
|
|
53
|
+
}
|
|
54
|
+
const filtered = {};
|
|
55
|
+
for (const key in request) {
|
|
56
|
+
if (/^_/.test(key)) {
|
|
57
|
+
continue; // 跳过 _ 开头
|
|
58
|
+
}
|
|
59
|
+
// 去掉 Symbol 类型的 key
|
|
60
|
+
if (typeof key === 'symbol') {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
// 去掉函数
|
|
64
|
+
if (typeof request[key] === 'function') {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
filtered[key] = request[key];
|
|
68
|
+
}
|
|
69
|
+
return filtered;
|
|
70
|
+
};
|
|
71
|
+
// 处理axios错误
|
|
72
|
+
const loggerError = (err) => {
|
|
73
|
+
// 错误时的请求头
|
|
74
|
+
logger.error('[axios] error', {
|
|
75
|
+
config: {
|
|
76
|
+
headers: filterHeaders(err?.config?.headers),
|
|
77
|
+
params: err?.config?.params,
|
|
78
|
+
data: err?.config?.data
|
|
79
|
+
},
|
|
80
|
+
response: {
|
|
81
|
+
status: err?.response?.status,
|
|
82
|
+
statusText: err?.response?.statusText,
|
|
83
|
+
headers: filterHeaders(err?.response?.headers),
|
|
84
|
+
config: filterConfig(err?.response?.config),
|
|
85
|
+
request: filterRequest(err?.response?.request),
|
|
86
|
+
data: err?.response?.data
|
|
87
|
+
},
|
|
88
|
+
message: err?.message
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* 基础请求
|
|
93
|
+
* @param service
|
|
94
|
+
* @param options
|
|
95
|
+
* @returns
|
|
96
|
+
*/
|
|
97
|
+
const createAxiosInstance = (service, options) => {
|
|
98
|
+
return new Promise((resolve, reject) => {
|
|
99
|
+
service(options)
|
|
100
|
+
.then(res => resolve(res?.data ?? {}))
|
|
101
|
+
.catch(err => {
|
|
102
|
+
loggerError(err);
|
|
103
|
+
// 丢出错误中携带的响应数据
|
|
104
|
+
reject(err?.response?.data);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export { createAxiosInstance };
|
|
@@ -37,10 +37,12 @@ class WebhookAPI {
|
|
|
37
37
|
*/
|
|
38
38
|
getKey() {
|
|
39
39
|
let seed = this.config.secret;
|
|
40
|
-
if (!seed)
|
|
40
|
+
if (!seed) {
|
|
41
41
|
throw new Error("secret not set, can't calc ed25519 key");
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
}
|
|
43
|
+
while (seed.length < 32) {
|
|
44
|
+
seed = seed.repeat(2);
|
|
45
|
+
} // Ed25519 的种子大小是 32 字节
|
|
44
46
|
seed = seed.slice(0, 32); // 修剪到 32 字节
|
|
45
47
|
const privateKey = Buffer.from(seed);
|
|
46
48
|
return {
|