@alemonjs/qq-bot 2.1.31 → 2.1.32
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 +14 -0
- package/dist/assets/index.css +1 -1
- package/dist/assets/index.js +3 -3
- package/dist/index.html +1 -1
- package/lib/config.d.ts +8 -0
- package/lib/config.js +23 -1
- package/lib/desktop.js +8 -1
- package/lib/index.js +5 -8
- package/lib/index.webhook.js +3 -0
- package/lib/index.websoket.d.ts +2 -0
- package/lib/index.websoket.js +18 -16
- package/lib/message/group/C2C_MESSAGE_CREATE.d.ts +7 -0
- package/lib/message/group/GROUP_AT_MESSAGE_CREATE.d.ts +7 -0
- package/lib/message/group/GROUP_MESSAGE_CREATE.d.ts +7 -0
- package/lib/register.d.ts +10 -1
- package/lib/register.js +260 -9
- package/lib/sdk/api.d.ts +58 -0
- package/lib/sdk/api.js +252 -14
- package/lib/sdk/client.webhook.js +14 -14
- package/lib/sdk/client.websoket.d.ts +2 -1
- package/lib/sdk/client.websoket.js +321 -110
- package/lib/sdk/config.d.ts +3 -1
- package/lib/sdk/config.js +6 -2
- package/lib/sdk/registry.d.ts +25 -0
- package/lib/sdk/registry.js +116 -0
- package/lib/sdk/session.d.ts +18 -0
- package/lib/sdk/session.js +56 -0
- package/lib/sdk/typing.d.ts +2 -0
- package/lib/sends.js +25 -4
- package/package.json +4 -3
package/lib/sdk/api.d.ts
CHANGED
|
@@ -1,9 +1,51 @@
|
|
|
1
1
|
import { type AxiosRequestConfig } from 'axios';
|
|
2
2
|
import { ApiRequestData, FileType } from './typing.js';
|
|
3
|
+
import { QQBotConfig } from './config.js';
|
|
3
4
|
export declare const BOTS_API_RUL = "https://bots.qq.com";
|
|
4
5
|
export declare const API_URL_SANDBOX = "https://sandbox.api.sgroup.qq.com";
|
|
5
6
|
export declare const API_URL = "https://api.sgroup.qq.com";
|
|
7
|
+
export type QQBotConnectionState = 'idle' | 'connecting' | 'ready' | 'reconnecting' | 'offline' | 'stopped';
|
|
8
|
+
export type QQBotConnectionStatus = {
|
|
9
|
+
state: QQBotConnectionState;
|
|
10
|
+
transport: 'websocket' | 'webhook' | 'proxy' | null;
|
|
11
|
+
reconnectAttempts: number;
|
|
12
|
+
heartbeatAcknowledged: boolean;
|
|
13
|
+
sessionId?: string;
|
|
14
|
+
sequence?: number;
|
|
15
|
+
resumed: boolean;
|
|
16
|
+
lastError?: string;
|
|
17
|
+
};
|
|
18
|
+
export type QQBotMediaHashes = {
|
|
19
|
+
sha256: string;
|
|
20
|
+
md5: string;
|
|
21
|
+
sha1: string;
|
|
22
|
+
md5_10m: string;
|
|
23
|
+
};
|
|
6
24
|
export declare class QQBotAPI {
|
|
25
|
+
#private;
|
|
26
|
+
protected readonly config: QQBotConfig;
|
|
27
|
+
constructor(options?: Record<string, unknown>);
|
|
28
|
+
getConnectionStatus(): QQBotConnectionStatus;
|
|
29
|
+
protected updateConnectionStatus(patch: Partial<QQBotConnectionStatus>): void;
|
|
30
|
+
setStreamLifecycleListener(listener?: (streamId: string) => void): void;
|
|
31
|
+
sendTyping(params: {
|
|
32
|
+
BotId?: string;
|
|
33
|
+
userOpenId: string;
|
|
34
|
+
msgId?: string;
|
|
35
|
+
durationSec?: number;
|
|
36
|
+
}): Promise<any>;
|
|
37
|
+
streamOpen(params: {
|
|
38
|
+
BotId?: string;
|
|
39
|
+
userOpenId: string;
|
|
40
|
+
msgId: string;
|
|
41
|
+
eventId?: string;
|
|
42
|
+
}): {
|
|
43
|
+
streamId: `${string}-${string}-${string}-${string}-${string}`;
|
|
44
|
+
};
|
|
45
|
+
streamUpdate(streamId: string, fullText: string): Promise<unknown>;
|
|
46
|
+
streamComplete(streamId: string): Promise<any>;
|
|
47
|
+
streamCancel(streamId: string): void;
|
|
48
|
+
protected cancelStreams(): void;
|
|
7
49
|
getAuthentication(): Promise<any>;
|
|
8
50
|
groupService(options: AxiosRequestConfig): Promise<any>;
|
|
9
51
|
guildServer(options: AxiosRequestConfig): Promise<any>;
|
|
@@ -37,6 +79,22 @@ export declare class QQBotAPI {
|
|
|
37
79
|
file_info: string;
|
|
38
80
|
ttl: number;
|
|
39
81
|
}>;
|
|
82
|
+
getMediaFileHashes(filePath: string): Promise<QQBotMediaHashes>;
|
|
83
|
+
postChunkedRichMedia(params: {
|
|
84
|
+
scope: 'group' | 'c2c';
|
|
85
|
+
targetId: string;
|
|
86
|
+
fileType: FileType;
|
|
87
|
+
data?: Buffer;
|
|
88
|
+
filePath?: string;
|
|
89
|
+
size?: number;
|
|
90
|
+
hashes?: QQBotMediaHashes;
|
|
91
|
+
name?: string;
|
|
92
|
+
onProgress?: (uploaded: number, total: number) => void;
|
|
93
|
+
}): Promise<{
|
|
94
|
+
file_uuid: string;
|
|
95
|
+
file_info: string;
|
|
96
|
+
ttl: number;
|
|
97
|
+
}>;
|
|
40
98
|
userMessageDelete(openid: string, message_id: string): Promise<any>;
|
|
41
99
|
grouMessageDelte(group_openid: string, message_id: string): Promise<any>;
|
|
42
100
|
channelsMessages(channel_id: string, message: {
|
package/lib/sdk/api.js
CHANGED
|
@@ -1,18 +1,153 @@
|
|
|
1
1
|
import axios from 'axios';
|
|
2
|
-
import {
|
|
2
|
+
import { QQBotConfig } from './config.js';
|
|
3
3
|
import FormData from 'form-data';
|
|
4
4
|
import { createPicFrom } from 'alemonjs/utils';
|
|
5
5
|
import { createAxiosInstance } from './instance.js';
|
|
6
|
+
import { randomUUID, createHash } from 'node:crypto';
|
|
7
|
+
import { createReadStream } from 'node:fs';
|
|
6
8
|
|
|
7
9
|
const BOTS_API_RUL = 'https://bots.qq.com';
|
|
8
10
|
const API_URL_SANDBOX = 'https://sandbox.api.sgroup.qq.com';
|
|
9
11
|
const API_URL = 'https://api.sgroup.qq.com';
|
|
10
|
-
const msgMap = new Map();
|
|
11
12
|
class QQBotAPI {
|
|
13
|
+
config;
|
|
14
|
+
#msgMap = new Map();
|
|
15
|
+
#streams = new Map();
|
|
16
|
+
#onStreamClosed;
|
|
17
|
+
#connectionStatus = {
|
|
18
|
+
state: 'idle',
|
|
19
|
+
transport: null,
|
|
20
|
+
reconnectAttempts: 0,
|
|
21
|
+
heartbeatAcknowledged: false,
|
|
22
|
+
resumed: false
|
|
23
|
+
};
|
|
24
|
+
constructor(options = {}) {
|
|
25
|
+
this.config = new QQBotConfig(options);
|
|
26
|
+
}
|
|
27
|
+
getConnectionStatus() {
|
|
28
|
+
return { ...this.#connectionStatus };
|
|
29
|
+
}
|
|
30
|
+
updateConnectionStatus(patch) {
|
|
31
|
+
this.#connectionStatus = { ...this.#connectionStatus, ...patch };
|
|
32
|
+
}
|
|
33
|
+
setStreamLifecycleListener(listener) {
|
|
34
|
+
this.#onStreamClosed = listener;
|
|
35
|
+
}
|
|
36
|
+
sendTyping(params) {
|
|
37
|
+
if (!params.userOpenId)
|
|
38
|
+
throw new Error('userOpenId is required');
|
|
39
|
+
return this.groupService({
|
|
40
|
+
url: `/v2/users/${params.userOpenId}/input_notify`,
|
|
41
|
+
method: 'post',
|
|
42
|
+
data: {
|
|
43
|
+
...(params.msgId && { msg_id: params.msgId }),
|
|
44
|
+
input_second: Math.max(1, Math.min(Number(params.durationSec) || 30, 60))
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
streamOpen(params) {
|
|
49
|
+
if (!params.userOpenId || !params.msgId)
|
|
50
|
+
throw new Error('C2C streaming requires userOpenId and msgId');
|
|
51
|
+
const streamId = randomUUID();
|
|
52
|
+
const stream = {
|
|
53
|
+
userOpenId: params.userOpenId,
|
|
54
|
+
msgId: params.msgId,
|
|
55
|
+
eventId: params.eventId || params.msgId,
|
|
56
|
+
msgSeq: this.getMessageSeq(params.msgId),
|
|
57
|
+
index: 0,
|
|
58
|
+
lastSentAt: 0,
|
|
59
|
+
lastSentText: ''
|
|
60
|
+
};
|
|
61
|
+
stream.timer = setTimeout(() => this.streamCancel(streamId), 5 * 60 * 1000);
|
|
62
|
+
this.#streams.set(streamId, stream);
|
|
63
|
+
return { streamId };
|
|
64
|
+
}
|
|
65
|
+
async streamUpdate(streamId, fullText) {
|
|
66
|
+
const stream = this.#streams.get(streamId);
|
|
67
|
+
if (!stream)
|
|
68
|
+
throw new Error('Unknown or expired streamId');
|
|
69
|
+
stream.latest = fullText;
|
|
70
|
+
if (!stream.sendTask) {
|
|
71
|
+
stream.sendTask = (async () => {
|
|
72
|
+
let result;
|
|
73
|
+
while (stream.latest !== undefined && this.#streams.get(streamId) === stream) {
|
|
74
|
+
const content = stream.latest;
|
|
75
|
+
stream.latest = undefined;
|
|
76
|
+
const wait = Math.max(0, 500 - (Date.now() - stream.lastSentAt));
|
|
77
|
+
if (wait)
|
|
78
|
+
await new Promise(resolve => setTimeout(resolve, wait));
|
|
79
|
+
if (this.#streams.get(streamId) !== stream)
|
|
80
|
+
break;
|
|
81
|
+
stream.lastSentAt = Date.now();
|
|
82
|
+
const response = await this.groupService({
|
|
83
|
+
url: `/v2/users/${stream.userOpenId}/stream_messages`,
|
|
84
|
+
method: 'post',
|
|
85
|
+
data: {
|
|
86
|
+
msg_id: stream.msgId,
|
|
87
|
+
event_id: stream.eventId,
|
|
88
|
+
msg_seq: stream.msgSeq,
|
|
89
|
+
index: stream.index++,
|
|
90
|
+
input_mode: 'replace',
|
|
91
|
+
input_state: 1,
|
|
92
|
+
content_type: 'markdown',
|
|
93
|
+
content_raw: content,
|
|
94
|
+
...(stream.streamMessageId && { stream_msg_id: stream.streamMessageId })
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
if (response?.id && !stream.streamMessageId)
|
|
98
|
+
stream.streamMessageId = response.id;
|
|
99
|
+
stream.lastSentText = content;
|
|
100
|
+
result = response;
|
|
101
|
+
}
|
|
102
|
+
return result;
|
|
103
|
+
})().finally(() => {
|
|
104
|
+
if (this.#streams.get(streamId) === stream)
|
|
105
|
+
stream.sendTask = undefined;
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return stream.sendTask;
|
|
109
|
+
}
|
|
110
|
+
async streamComplete(streamId) {
|
|
111
|
+
const stream = this.#streams.get(streamId);
|
|
112
|
+
if (!stream)
|
|
113
|
+
throw new Error('Unknown or expired streamId');
|
|
114
|
+
try {
|
|
115
|
+
await stream.sendTask;
|
|
116
|
+
return await this.groupService({
|
|
117
|
+
url: `/v2/users/${stream.userOpenId}/stream_messages`,
|
|
118
|
+
method: 'post',
|
|
119
|
+
data: {
|
|
120
|
+
msg_id: stream.msgId,
|
|
121
|
+
event_id: stream.eventId,
|
|
122
|
+
msg_seq: stream.msgSeq,
|
|
123
|
+
index: stream.index++,
|
|
124
|
+
input_mode: 'replace',
|
|
125
|
+
input_state: 10,
|
|
126
|
+
content_type: 'markdown',
|
|
127
|
+
content_raw: stream.latest ?? stream.lastSentText,
|
|
128
|
+
...(stream.streamMessageId && { stream_msg_id: stream.streamMessageId })
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
this.streamCancel(streamId);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
streamCancel(streamId) {
|
|
137
|
+
const stream = this.#streams.get(streamId);
|
|
138
|
+
if (stream?.timer)
|
|
139
|
+
clearTimeout(stream.timer);
|
|
140
|
+
if (this.#streams.delete(streamId))
|
|
141
|
+
this.#onStreamClosed?.(streamId);
|
|
142
|
+
}
|
|
143
|
+
cancelStreams() {
|
|
144
|
+
for (const streamId of this.#streams.keys())
|
|
145
|
+
this.streamCancel(streamId);
|
|
146
|
+
}
|
|
12
147
|
getAuthentication() {
|
|
13
|
-
const app_id = config.get('app_id');
|
|
14
|
-
const secret = config.get('secret');
|
|
15
|
-
const baseUrlAppAccessToken = config.get('base_url_app_access_token');
|
|
148
|
+
const app_id = this.config.get('app_id');
|
|
149
|
+
const secret = this.config.get('secret');
|
|
150
|
+
const baseUrlAppAccessToken = this.config.get('base_url_app_access_token');
|
|
16
151
|
const params = {
|
|
17
152
|
url: '/app/getAppAccessToken'
|
|
18
153
|
};
|
|
@@ -33,9 +168,9 @@ class QQBotAPI {
|
|
|
33
168
|
});
|
|
34
169
|
}
|
|
35
170
|
groupService(options) {
|
|
36
|
-
const app_id = config.get('app_id');
|
|
37
|
-
const token = config.get('access_token');
|
|
38
|
-
const sandbox = config.get('sandbox');
|
|
171
|
+
const app_id = this.config.get('app_id');
|
|
172
|
+
const token = this.config.get('access_token');
|
|
173
|
+
const sandbox = this.config.get('sandbox');
|
|
39
174
|
const service = axios.create({
|
|
40
175
|
baseURL: sandbox ? API_URL_SANDBOX : API_URL,
|
|
41
176
|
timeout: 20000,
|
|
@@ -50,7 +185,7 @@ class QQBotAPI {
|
|
|
50
185
|
return this.groupService(options);
|
|
51
186
|
}
|
|
52
187
|
gateway() {
|
|
53
|
-
const baseUrlGateway = config.get('base_url_gateway');
|
|
188
|
+
const baseUrlGateway = this.config.get('base_url_gateway');
|
|
54
189
|
const params = {
|
|
55
190
|
url: '/gateway'
|
|
56
191
|
};
|
|
@@ -75,13 +210,13 @@ class QQBotAPI {
|
|
|
75
210
|
});
|
|
76
211
|
}
|
|
77
212
|
getMessageSeq(MessageId) {
|
|
78
|
-
let seq = msgMap.get(MessageId) || 0;
|
|
213
|
+
let seq = this.#msgMap.get(MessageId) || 0;
|
|
79
214
|
seq++;
|
|
80
|
-
msgMap.set(MessageId, seq);
|
|
81
|
-
if (msgMap.size > 100) {
|
|
82
|
-
const firstKey = msgMap.keys().next().value;
|
|
215
|
+
this.#msgMap.set(MessageId, seq);
|
|
216
|
+
if (this.#msgMap.size > 100) {
|
|
217
|
+
const firstKey = this.#msgMap.keys().next().value;
|
|
83
218
|
if (firstKey) {
|
|
84
|
-
msgMap.delete(firstKey);
|
|
219
|
+
this.#msgMap.delete(firstKey);
|
|
85
220
|
}
|
|
86
221
|
}
|
|
87
222
|
return seq;
|
|
@@ -118,6 +253,109 @@ class QQBotAPI {
|
|
|
118
253
|
}
|
|
119
254
|
});
|
|
120
255
|
}
|
|
256
|
+
async getMediaFileHashes(filePath) {
|
|
257
|
+
const sha256 = createHash('sha256');
|
|
258
|
+
const md5 = createHash('md5');
|
|
259
|
+
const sha1 = createHash('sha1');
|
|
260
|
+
const md5_10m = createHash('md5');
|
|
261
|
+
let firstBytes = 0;
|
|
262
|
+
const firstLimit = 10_002_432;
|
|
263
|
+
for await (const chunk of createReadStream(filePath)) {
|
|
264
|
+
const data = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
265
|
+
sha256.update(data);
|
|
266
|
+
md5.update(data);
|
|
267
|
+
sha1.update(data);
|
|
268
|
+
if (firstBytes < firstLimit) {
|
|
269
|
+
const length = Math.min(data.length, firstLimit - firstBytes);
|
|
270
|
+
md5_10m.update(data.subarray(0, length));
|
|
271
|
+
firstBytes += length;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return {
|
|
275
|
+
sha256: sha256.digest('hex'),
|
|
276
|
+
md5: md5.digest('hex'),
|
|
277
|
+
sha1: sha1.digest('hex'),
|
|
278
|
+
md5_10m: md5_10m.digest('hex')
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
async postChunkedRichMedia(params) {
|
|
282
|
+
const hasBuffer = Buffer.isBuffer(params.data);
|
|
283
|
+
const hasFile = Boolean(params.filePath);
|
|
284
|
+
if (hasBuffer === hasFile)
|
|
285
|
+
throw new Error('Chunked media requires exactly one of data or filePath');
|
|
286
|
+
const total = hasBuffer ? params.data.length : Number(params.size);
|
|
287
|
+
if (!Number.isSafeInteger(total) || total < 1)
|
|
288
|
+
throw new Error('Chunked media requires a valid file size');
|
|
289
|
+
const hashes = params.hashes || (hasBuffer ? this.#getMediaBufferHashes(params.data) : await this.getMediaFileHashes(params.filePath));
|
|
290
|
+
const prefix = params.scope === 'group' ? `/v2/groups/${params.targetId}` : `/v2/users/${params.targetId}`;
|
|
291
|
+
const prepared = await this.groupService({
|
|
292
|
+
url: `${prefix}/upload_prepare`,
|
|
293
|
+
method: 'post',
|
|
294
|
+
data: {
|
|
295
|
+
file_type: params.fileType,
|
|
296
|
+
file_name: params.name || 'file',
|
|
297
|
+
file_size: total,
|
|
298
|
+
md5: hashes.md5,
|
|
299
|
+
sha1: hashes.sha1,
|
|
300
|
+
md5_10m: hashes.md5_10m
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
let uploaded = 0;
|
|
304
|
+
for (const part of prepared.parts || []) {
|
|
305
|
+
const offset = (part.index - 1) * prepared.block_size;
|
|
306
|
+
const length = Math.min(prepared.block_size, total - offset);
|
|
307
|
+
if (length <= 0)
|
|
308
|
+
throw new Error(`QQ returned invalid upload part index: ${part.index}`);
|
|
309
|
+
const body = hasBuffer ? params.data.subarray(offset, offset + length) : undefined;
|
|
310
|
+
const partMd5 = body ? createHash('md5').update(body).digest('hex') : await this.#getFilePartHash(params.filePath, offset, length);
|
|
311
|
+
await this.#retry(() => axios.put(part.presigned_url, body || createReadStream(params.filePath, { start: offset, end: offset + length - 1 }), {
|
|
312
|
+
timeout: 300_000,
|
|
313
|
+
maxBodyLength: Infinity,
|
|
314
|
+
maxContentLength: Infinity,
|
|
315
|
+
headers: { 'Content-Length': String(length) }
|
|
316
|
+
}));
|
|
317
|
+
await this.#retry(() => this.groupService({
|
|
318
|
+
url: `${prefix}/upload_part_finish`,
|
|
319
|
+
method: 'post',
|
|
320
|
+
data: { upload_id: prepared.upload_id, part_index: part.index, block_size: length, md5: partMd5 }
|
|
321
|
+
}));
|
|
322
|
+
uploaded += length;
|
|
323
|
+
params.onProgress?.(uploaded, total);
|
|
324
|
+
}
|
|
325
|
+
return this.#retry(() => this.groupService({
|
|
326
|
+
url: `${prefix}/complete_upload`,
|
|
327
|
+
method: 'post',
|
|
328
|
+
data: { upload_id: prepared.upload_id }
|
|
329
|
+
}));
|
|
330
|
+
}
|
|
331
|
+
#getMediaBufferHashes(data) {
|
|
332
|
+
return {
|
|
333
|
+
sha256: createHash('sha256').update(data).digest('hex'),
|
|
334
|
+
md5: createHash('md5').update(data).digest('hex'),
|
|
335
|
+
sha1: createHash('sha1').update(data).digest('hex'),
|
|
336
|
+
md5_10m: createHash('md5').update(data.subarray(0, 10_002_432)).digest('hex')
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
async #getFilePartHash(filePath, start, length) {
|
|
340
|
+
const md5 = createHash('md5');
|
|
341
|
+
for await (const chunk of createReadStream(filePath, { start, end: start + length - 1 }))
|
|
342
|
+
md5.update(chunk);
|
|
343
|
+
return md5.digest('hex');
|
|
344
|
+
}
|
|
345
|
+
async #retry(operation, attempts = 3) {
|
|
346
|
+
let lastError;
|
|
347
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
348
|
+
try {
|
|
349
|
+
return await operation();
|
|
350
|
+
}
|
|
351
|
+
catch (error) {
|
|
352
|
+
lastError = error;
|
|
353
|
+
if (attempt + 1 < attempts)
|
|
354
|
+
await new Promise(resolve => setTimeout(resolve, 250 * 2 ** attempt));
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
throw lastError;
|
|
358
|
+
}
|
|
121
359
|
userMessageDelete(openid, message_id) {
|
|
122
360
|
return this.groupService({
|
|
123
361
|
url: `/v2/users/${openid}/messages/${message_id}`,
|
|
@@ -3,7 +3,6 @@ import bodyParser from 'koa-bodyparser';
|
|
|
3
3
|
import Router from 'koa-router';
|
|
4
4
|
import { WebhookAPI } from './webhook.secret.js';
|
|
5
5
|
import Koa from 'koa';
|
|
6
|
-
import { config } from './config.js';
|
|
7
6
|
import { v4 } from 'uuid';
|
|
8
7
|
import { WebSocketServer, WebSocket } from 'ws';
|
|
9
8
|
|
|
@@ -23,10 +22,7 @@ class QQBotClient extends QQBotAPI {
|
|
|
23
22
|
#client = [];
|
|
24
23
|
#ws = null;
|
|
25
24
|
constructor(opstion) {
|
|
26
|
-
super();
|
|
27
|
-
for (const key in opstion) {
|
|
28
|
-
config.set(key, opstion[key]);
|
|
29
|
-
}
|
|
25
|
+
super(opstion);
|
|
30
26
|
}
|
|
31
27
|
on(key, val) {
|
|
32
28
|
if (!this.#events[key]) {
|
|
@@ -37,13 +33,13 @@ class QQBotClient extends QQBotAPI {
|
|
|
37
33
|
}
|
|
38
34
|
async #setTimeoutBotConfig() {
|
|
39
35
|
const callBack = async () => {
|
|
40
|
-
const app_id = config.get('app_id');
|
|
41
|
-
const secret = config.get('secret');
|
|
36
|
+
const app_id = this.config.get('app_id');
|
|
37
|
+
const secret = this.config.get('secret');
|
|
42
38
|
if (!app_id || !secret) {
|
|
43
39
|
return;
|
|
44
40
|
}
|
|
45
41
|
const data = await this.getAuthentication();
|
|
46
|
-
config.set('access_token', data.access_token);
|
|
42
|
+
this.config.set('access_token', data.access_token);
|
|
47
43
|
console.info('refresh', data.expires_in, 's');
|
|
48
44
|
setTimeout(() => void callBack(), data.expires_in * 1000);
|
|
49
45
|
};
|
|
@@ -51,15 +47,16 @@ class QQBotClient extends QQBotAPI {
|
|
|
51
47
|
}
|
|
52
48
|
connect() {
|
|
53
49
|
try {
|
|
54
|
-
const ws = config.get('ws');
|
|
50
|
+
const ws = this.config.get('ws');
|
|
51
|
+
this.updateConnectionStatus({ state: 'connecting', transport: ws ? 'proxy' : 'webhook' });
|
|
55
52
|
if (!ws) {
|
|
56
53
|
void this.#setTimeoutBotConfig();
|
|
57
54
|
this.#app = new Koa();
|
|
58
55
|
this.#app.use(bodyParser());
|
|
59
56
|
const router = new Router();
|
|
60
|
-
const port = config.get('port');
|
|
61
|
-
const secret = config.get('secret');
|
|
62
|
-
const route = config.get('route') ?? '/webhook';
|
|
57
|
+
const port = this.config.get('port');
|
|
58
|
+
const secret = this.config.get('secret');
|
|
59
|
+
const route = this.config.get('route') ?? '/webhook';
|
|
63
60
|
const cfg = {
|
|
64
61
|
secret: secret ?? '',
|
|
65
62
|
port: port ? Number(port) : 17157
|
|
@@ -96,7 +93,7 @@ class QQBotClient extends QQBotAPI {
|
|
|
96
93
|
for (const event of this.#events[body.t] || []) {
|
|
97
94
|
event(body.d);
|
|
98
95
|
}
|
|
99
|
-
const accessToken = config.get('access_token');
|
|
96
|
+
const accessToken = this.config.get('access_token');
|
|
100
97
|
for (const client of this.#client) {
|
|
101
98
|
try {
|
|
102
99
|
if (accessToken) {
|
|
@@ -114,6 +111,7 @@ class QQBotClient extends QQBotAPI {
|
|
|
114
111
|
this.#app.use(router.allowedMethods());
|
|
115
112
|
const server = this.#app.listen(cfg.port, () => {
|
|
116
113
|
console.log('Server running at http://localhost:' + cfg.port + route);
|
|
114
|
+
this.updateConnectionStatus({ state: 'ready' });
|
|
117
115
|
});
|
|
118
116
|
const wss = new WebSocketServer({ server: server });
|
|
119
117
|
console.log('Server running at ws://localhost:' + cfg.port + '/');
|
|
@@ -144,6 +142,7 @@ class QQBotClient extends QQBotAPI {
|
|
|
144
142
|
this.#ws = new WebSocket(ws);
|
|
145
143
|
this.#ws.on('open', () => {
|
|
146
144
|
this.#count = 0;
|
|
145
|
+
this.updateConnectionStatus({ state: 'ready' });
|
|
147
146
|
console.log('ws connected');
|
|
148
147
|
});
|
|
149
148
|
this.#ws.on('message', data => {
|
|
@@ -151,7 +150,7 @@ class QQBotClient extends QQBotAPI {
|
|
|
151
150
|
const body = normalizeGatewayMessage(JSON.parse(data.toString()));
|
|
152
151
|
const accessToken = body['access_token'];
|
|
153
152
|
if (accessToken) {
|
|
154
|
-
config.set('access_token', accessToken);
|
|
153
|
+
this.config.set('access_token', accessToken);
|
|
155
154
|
}
|
|
156
155
|
for (const event of this.#events[body.t] || []) {
|
|
157
156
|
event(body.d);
|
|
@@ -162,6 +161,7 @@ class QQBotClient extends QQBotAPI {
|
|
|
162
161
|
}
|
|
163
162
|
});
|
|
164
163
|
this.#ws.on('close', () => {
|
|
164
|
+
this.updateConnectionStatus({ state: 'offline' });
|
|
165
165
|
console.log('ws closed');
|
|
166
166
|
if (this.#count > 5) {
|
|
167
167
|
return;
|
|
@@ -3,7 +3,8 @@ import { QQBotEventMap } from './message.js';
|
|
|
3
3
|
import { Options } from './typing.js';
|
|
4
4
|
export declare class QQBotClients extends QQBotAPI {
|
|
5
5
|
#private;
|
|
6
|
-
constructor(
|
|
6
|
+
constructor(option: Options);
|
|
7
|
+
disconnect(): void;
|
|
7
8
|
on<T extends keyof QQBotEventMap>(key: T, val: (event: QQBotEventMap[T]) => any): this;
|
|
8
9
|
connect(gatewayURL?: string): Promise<void>;
|
|
9
10
|
}
|