@alemonjs/qq-bot 2.1.31 → 2.1.33
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 +1950 -25
- 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/hook.d.ts +2 -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 +20 -18
- 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_JOIN_REQUEST.d.ts +23 -0
- package/lib/message/group/GROUP_JOIN_REQUEST.js +1 -0
- package/lib/message/group/GROUP_MESSAGE_CREATE.d.ts +7 -0
- package/lib/register.d.ts +10 -1
- package/lib/register.js +508 -53
- package/lib/sdk/api.d.ts +116 -3
- package/lib/sdk/api.js +390 -17
- 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/instance.js +2 -0
- package/lib/sdk/intents.d.ts +1 -1
- package/lib/sdk/intents.js +2 -0
- package/lib/sdk/message.group.d.ts +2 -0
- 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 +125 -1
- package/lib/sends.d.ts +6 -2
- package/lib/sends.js +136 -57
- package/lib/upload.d.ts +21 -0
- package/lib/upload.js +60 -0
- package/package.json +6 -5
package/lib/sdk/api.d.ts
CHANGED
|
@@ -1,9 +1,51 @@
|
|
|
1
1
|
import { type AxiosRequestConfig } from 'axios';
|
|
2
|
-
import { ApiRequestData, FileType } from './typing.js';
|
|
3
|
-
|
|
2
|
+
import { ApiRequestData, FileType, GroupAction, SetMemberMuteState, StreamMessageData, UploadPartFinishData, UploadPrepareData } from './typing.js';
|
|
3
|
+
import { QQBotConfig } from './config.js';
|
|
4
|
+
export declare const BOTS_API_RUL = "https://api.bot.qq.com";
|
|
4
5
|
export declare const API_URL_SANDBOX = "https://sandbox.api.sgroup.qq.com";
|
|
5
|
-
export declare const API_URL = "https://api.
|
|
6
|
+
export declare const API_URL = "https://api.bot.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>;
|
|
@@ -22,23 +64,94 @@ export declare class QQBotAPI {
|
|
|
22
64
|
file_type: FileType;
|
|
23
65
|
url?: string;
|
|
24
66
|
file_data?: any;
|
|
67
|
+
upload_id?: string;
|
|
68
|
+
file_name?: string;
|
|
25
69
|
}): Promise<{
|
|
26
70
|
file_uuid: string;
|
|
27
71
|
file_info: string;
|
|
28
72
|
ttl: number;
|
|
73
|
+
id?: string;
|
|
74
|
+
raw_url?: string;
|
|
29
75
|
}>;
|
|
30
76
|
postRichMediaByGroup(openid: string, data: {
|
|
31
77
|
srv_send_msg?: boolean;
|
|
32
78
|
file_type: FileType;
|
|
33
79
|
url?: string;
|
|
34
80
|
file_data?: any;
|
|
81
|
+
upload_id?: string;
|
|
82
|
+
file_name?: string;
|
|
83
|
+
}): Promise<{
|
|
84
|
+
file_uuid: string;
|
|
85
|
+
file_info: string;
|
|
86
|
+
ttl: number;
|
|
87
|
+
id?: string;
|
|
88
|
+
raw_url?: string;
|
|
89
|
+
}>;
|
|
90
|
+
getMediaFileHashes(filePath: string): Promise<QQBotMediaHashes>;
|
|
91
|
+
postChunkedRichMedia(params: {
|
|
92
|
+
scope: 'group' | 'c2c';
|
|
93
|
+
targetId: string;
|
|
94
|
+
fileType: FileType;
|
|
95
|
+
data?: Buffer;
|
|
96
|
+
filePath?: string;
|
|
97
|
+
size?: number;
|
|
98
|
+
hashes?: QQBotMediaHashes;
|
|
99
|
+
name?: string;
|
|
100
|
+
onProgress?: (uploaded: number, total: number) => void;
|
|
35
101
|
}): Promise<{
|
|
36
102
|
file_uuid: string;
|
|
37
103
|
file_info: string;
|
|
38
104
|
ttl: number;
|
|
39
105
|
}>;
|
|
40
106
|
userMessageDelete(openid: string, message_id: string): Promise<any>;
|
|
107
|
+
groupMessageDelete(group_openid: string, message_id: string): Promise<any>;
|
|
41
108
|
grouMessageDelte(group_openid: string, message_id: string): Promise<any>;
|
|
109
|
+
groupsInfo(group_openid: string): Promise<any>;
|
|
110
|
+
groupsBotState(group_openid: string): Promise<any>;
|
|
111
|
+
groupsMembersMessage(group_openid: string, member_openid: string): Promise<any>;
|
|
112
|
+
groupsJoinRequestList(group_openid: string, params?: {
|
|
113
|
+
cursor?: string;
|
|
114
|
+
limit?: number;
|
|
115
|
+
}): Promise<any>;
|
|
116
|
+
groupsApprovalJoinRequest(group_openid: string, member_openid: string, data: {
|
|
117
|
+
op: 'approve' | 'decline';
|
|
118
|
+
join_request_id?: string;
|
|
119
|
+
reject_reason?: string;
|
|
120
|
+
add_to_member_blacklist?: boolean;
|
|
121
|
+
}): Promise<any>;
|
|
122
|
+
groupsRestrictChatSetting(group_openid: string): Promise<any>;
|
|
123
|
+
groupsRestrictChatSettingPost(group_openid: string, data: {
|
|
124
|
+
members?: SetMemberMuteState[];
|
|
125
|
+
}): Promise<any>;
|
|
126
|
+
groupsJoinApprovalStrategies(params?: {
|
|
127
|
+
cursor?: string;
|
|
128
|
+
limit?: number;
|
|
129
|
+
}): Promise<any>;
|
|
130
|
+
groupsJoinApprovalStrategyCreate(data: {
|
|
131
|
+
group_openids?: string[];
|
|
132
|
+
group_ids?: string[];
|
|
133
|
+
is_enable?: 'on' | 'off';
|
|
134
|
+
expire_at?: string;
|
|
135
|
+
remark?: string;
|
|
136
|
+
}): Promise<any>;
|
|
137
|
+
groupsJoinApprovalStrategyPatch(strategy_id: string, data: {
|
|
138
|
+
is_enable?: 'on' | 'off';
|
|
139
|
+
expire_at?: string;
|
|
140
|
+
group_action?: GroupAction;
|
|
141
|
+
remark?: string;
|
|
142
|
+
}): Promise<any>;
|
|
143
|
+
groupsJoinApprovalStrategyDelete(strategy_id: string): Promise<any>;
|
|
144
|
+
groupsJoinApprovalStrategyExecute(strategy_id: string): Promise<any>;
|
|
145
|
+
groupsJoinApprovalStrategyWhitelistUsers(strategy_id: string, data: {
|
|
146
|
+
op: 'add' | 'del';
|
|
147
|
+
whitelist_users: string[];
|
|
148
|
+
}): Promise<any>;
|
|
149
|
+
usersUploadPrepare(userId: string, data: UploadPrepareData): Promise<any>;
|
|
150
|
+
groupUploadPrepare(groupId: string, data: UploadPrepareData): Promise<any>;
|
|
151
|
+
usersUploadPartFinish(userId: string, data: UploadPartFinishData): Promise<any>;
|
|
152
|
+
groupUploadPartFinish(groupId: string, data: UploadPartFinishData): Promise<any>;
|
|
153
|
+
uploadPartDirect(presignedUrl: string, part: Buffer): Promise<import("axios").AxiosResponse<any, any, {}>>;
|
|
154
|
+
streamMessages(userId: string, data: StreamMessageData): Promise<any>;
|
|
42
155
|
channelsMessages(channel_id: string, message: {
|
|
43
156
|
content?: string;
|
|
44
157
|
embed?: any;
|
package/lib/sdk/api.js
CHANGED
|
@@ -1,18 +1,154 @@
|
|
|
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';
|
|
8
|
+
import { fileDataToBuffer, CHUNK_THRESHOLD, chunkedUpload } from '../upload.js';
|
|
6
9
|
|
|
7
|
-
const BOTS_API_RUL = 'https://
|
|
10
|
+
const BOTS_API_RUL = 'https://api.bot.qq.com';
|
|
8
11
|
const API_URL_SANDBOX = 'https://sandbox.api.sgroup.qq.com';
|
|
9
|
-
const API_URL = 'https://api.
|
|
10
|
-
const msgMap = new Map();
|
|
12
|
+
const API_URL = 'https://api.bot.qq.com';
|
|
11
13
|
class QQBotAPI {
|
|
14
|
+
config;
|
|
15
|
+
#msgMap = new Map();
|
|
16
|
+
#streams = new Map();
|
|
17
|
+
#onStreamClosed;
|
|
18
|
+
#connectionStatus = {
|
|
19
|
+
state: 'idle',
|
|
20
|
+
transport: null,
|
|
21
|
+
reconnectAttempts: 0,
|
|
22
|
+
heartbeatAcknowledged: false,
|
|
23
|
+
resumed: false
|
|
24
|
+
};
|
|
25
|
+
constructor(options = {}) {
|
|
26
|
+
this.config = new QQBotConfig(options);
|
|
27
|
+
}
|
|
28
|
+
getConnectionStatus() {
|
|
29
|
+
return { ...this.#connectionStatus };
|
|
30
|
+
}
|
|
31
|
+
updateConnectionStatus(patch) {
|
|
32
|
+
this.#connectionStatus = { ...this.#connectionStatus, ...patch };
|
|
33
|
+
}
|
|
34
|
+
setStreamLifecycleListener(listener) {
|
|
35
|
+
this.#onStreamClosed = listener;
|
|
36
|
+
}
|
|
37
|
+
sendTyping(params) {
|
|
38
|
+
if (!params.userOpenId)
|
|
39
|
+
throw new Error('userOpenId is required');
|
|
40
|
+
return this.groupService({
|
|
41
|
+
url: `/v2/users/${params.userOpenId}/input_notify`,
|
|
42
|
+
method: 'post',
|
|
43
|
+
data: {
|
|
44
|
+
...(params.msgId && { msg_id: params.msgId }),
|
|
45
|
+
input_second: Math.max(1, Math.min(Number(params.durationSec) || 30, 60))
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
streamOpen(params) {
|
|
50
|
+
if (!params.userOpenId || !params.msgId)
|
|
51
|
+
throw new Error('C2C streaming requires userOpenId and msgId');
|
|
52
|
+
const streamId = randomUUID();
|
|
53
|
+
const stream = {
|
|
54
|
+
userOpenId: params.userOpenId,
|
|
55
|
+
msgId: params.msgId,
|
|
56
|
+
eventId: params.eventId || params.msgId,
|
|
57
|
+
msgSeq: this.getMessageSeq(params.msgId),
|
|
58
|
+
index: 0,
|
|
59
|
+
lastSentAt: 0,
|
|
60
|
+
lastSentText: ''
|
|
61
|
+
};
|
|
62
|
+
stream.timer = setTimeout(() => this.streamCancel(streamId), 5 * 60 * 1000);
|
|
63
|
+
this.#streams.set(streamId, stream);
|
|
64
|
+
return { streamId };
|
|
65
|
+
}
|
|
66
|
+
async streamUpdate(streamId, fullText) {
|
|
67
|
+
const stream = this.#streams.get(streamId);
|
|
68
|
+
if (!stream)
|
|
69
|
+
throw new Error('Unknown or expired streamId');
|
|
70
|
+
stream.latest = fullText;
|
|
71
|
+
if (!stream.sendTask) {
|
|
72
|
+
stream.sendTask = (async () => {
|
|
73
|
+
let result;
|
|
74
|
+
while (stream.latest !== undefined && this.#streams.get(streamId) === stream) {
|
|
75
|
+
const content = stream.latest;
|
|
76
|
+
stream.latest = undefined;
|
|
77
|
+
const wait = Math.max(0, 500 - (Date.now() - stream.lastSentAt));
|
|
78
|
+
if (wait)
|
|
79
|
+
await new Promise(resolve => setTimeout(resolve, wait));
|
|
80
|
+
if (this.#streams.get(streamId) !== stream)
|
|
81
|
+
break;
|
|
82
|
+
stream.lastSentAt = Date.now();
|
|
83
|
+
const response = await this.groupService({
|
|
84
|
+
url: `/v2/users/${stream.userOpenId}/stream_messages`,
|
|
85
|
+
method: 'post',
|
|
86
|
+
data: {
|
|
87
|
+
msg_id: stream.msgId,
|
|
88
|
+
event_id: stream.eventId,
|
|
89
|
+
msg_seq: stream.msgSeq,
|
|
90
|
+
index: stream.index++,
|
|
91
|
+
input_mode: 'replace',
|
|
92
|
+
input_state: 1,
|
|
93
|
+
content_type: 'markdown',
|
|
94
|
+
content_raw: content,
|
|
95
|
+
...(stream.streamMessageId && { stream_msg_id: stream.streamMessageId })
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
if (response?.id && !stream.streamMessageId)
|
|
99
|
+
stream.streamMessageId = response.id;
|
|
100
|
+
stream.lastSentText = content;
|
|
101
|
+
result = response;
|
|
102
|
+
}
|
|
103
|
+
return result;
|
|
104
|
+
})().finally(() => {
|
|
105
|
+
if (this.#streams.get(streamId) === stream)
|
|
106
|
+
stream.sendTask = undefined;
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
return stream.sendTask;
|
|
110
|
+
}
|
|
111
|
+
async streamComplete(streamId) {
|
|
112
|
+
const stream = this.#streams.get(streamId);
|
|
113
|
+
if (!stream)
|
|
114
|
+
throw new Error('Unknown or expired streamId');
|
|
115
|
+
try {
|
|
116
|
+
await stream.sendTask;
|
|
117
|
+
return await this.groupService({
|
|
118
|
+
url: `/v2/users/${stream.userOpenId}/stream_messages`,
|
|
119
|
+
method: 'post',
|
|
120
|
+
data: {
|
|
121
|
+
msg_id: stream.msgId,
|
|
122
|
+
event_id: stream.eventId,
|
|
123
|
+
msg_seq: stream.msgSeq,
|
|
124
|
+
index: stream.index++,
|
|
125
|
+
input_mode: 'replace',
|
|
126
|
+
input_state: 10,
|
|
127
|
+
content_type: 'markdown',
|
|
128
|
+
content_raw: stream.latest ?? stream.lastSentText,
|
|
129
|
+
...(stream.streamMessageId && { stream_msg_id: stream.streamMessageId })
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
this.streamCancel(streamId);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
streamCancel(streamId) {
|
|
138
|
+
const stream = this.#streams.get(streamId);
|
|
139
|
+
if (stream?.timer)
|
|
140
|
+
clearTimeout(stream.timer);
|
|
141
|
+
if (this.#streams.delete(streamId))
|
|
142
|
+
this.#onStreamClosed?.(streamId);
|
|
143
|
+
}
|
|
144
|
+
cancelStreams() {
|
|
145
|
+
for (const streamId of this.#streams.keys())
|
|
146
|
+
this.streamCancel(streamId);
|
|
147
|
+
}
|
|
12
148
|
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');
|
|
149
|
+
const app_id = this.config.get('app_id');
|
|
150
|
+
const secret = this.config.get('secret');
|
|
151
|
+
const baseUrlAppAccessToken = this.config.get('base_url_app_access_token');
|
|
16
152
|
const params = {
|
|
17
153
|
url: '/app/getAppAccessToken'
|
|
18
154
|
};
|
|
@@ -33,9 +169,9 @@ class QQBotAPI {
|
|
|
33
169
|
});
|
|
34
170
|
}
|
|
35
171
|
groupService(options) {
|
|
36
|
-
const app_id = config.get('app_id');
|
|
37
|
-
const token = config.get('access_token');
|
|
38
|
-
const sandbox = config.get('sandbox');
|
|
172
|
+
const app_id = this.config.get('app_id');
|
|
173
|
+
const token = this.config.get('access_token');
|
|
174
|
+
const sandbox = this.config.get('sandbox');
|
|
39
175
|
const service = axios.create({
|
|
40
176
|
baseURL: sandbox ? API_URL_SANDBOX : API_URL,
|
|
41
177
|
timeout: 20000,
|
|
@@ -50,7 +186,7 @@ class QQBotAPI {
|
|
|
50
186
|
return this.groupService(options);
|
|
51
187
|
}
|
|
52
188
|
gateway() {
|
|
53
|
-
const baseUrlGateway = config.get('base_url_gateway');
|
|
189
|
+
const baseUrlGateway = this.config.get('base_url_gateway');
|
|
54
190
|
const params = {
|
|
55
191
|
url: '/gateway'
|
|
56
192
|
};
|
|
@@ -75,13 +211,13 @@ class QQBotAPI {
|
|
|
75
211
|
});
|
|
76
212
|
}
|
|
77
213
|
getMessageSeq(MessageId) {
|
|
78
|
-
let seq = msgMap.get(MessageId) || 0;
|
|
214
|
+
let seq = this.#msgMap.get(MessageId) || 0;
|
|
79
215
|
seq++;
|
|
80
|
-
msgMap.set(MessageId, seq);
|
|
81
|
-
if (msgMap.size > 100) {
|
|
82
|
-
const firstKey = msgMap.keys().next().value;
|
|
216
|
+
this.#msgMap.set(MessageId, seq);
|
|
217
|
+
if (this.#msgMap.size > 100) {
|
|
218
|
+
const firstKey = this.#msgMap.keys().next().value;
|
|
83
219
|
if (firstKey) {
|
|
84
|
-
msgMap.delete(firstKey);
|
|
220
|
+
this.#msgMap.delete(firstKey);
|
|
85
221
|
}
|
|
86
222
|
}
|
|
87
223
|
return seq;
|
|
@@ -102,6 +238,10 @@ class QQBotAPI {
|
|
|
102
238
|
});
|
|
103
239
|
}
|
|
104
240
|
postRichMediaByUser(openid, data) {
|
|
241
|
+
const fileBuffer = fileDataToBuffer(data.file_data);
|
|
242
|
+
if (!data.upload_id && fileBuffer && fileBuffer.byteLength > CHUNK_THRESHOLD) {
|
|
243
|
+
return chunkedUpload(this, 'user', openid, fileBuffer, { file_type: data.file_type, file_name: data.file_name, srv_send_msg: data.srv_send_msg });
|
|
244
|
+
}
|
|
105
245
|
return this.groupService({
|
|
106
246
|
url: `/v2/users/${openid}/files`,
|
|
107
247
|
method: 'post',
|
|
@@ -109,6 +249,10 @@ class QQBotAPI {
|
|
|
109
249
|
});
|
|
110
250
|
}
|
|
111
251
|
postRichMediaByGroup(openid, data) {
|
|
252
|
+
const fileBuffer = fileDataToBuffer(data.file_data);
|
|
253
|
+
if (!data.upload_id && fileBuffer && fileBuffer.byteLength > CHUNK_THRESHOLD) {
|
|
254
|
+
return chunkedUpload(this, 'group', openid, fileBuffer, { file_type: data.file_type, file_name: data.file_name, srv_send_msg: data.srv_send_msg });
|
|
255
|
+
}
|
|
112
256
|
return this.groupService({
|
|
113
257
|
url: `/v2/groups/${openid}/files`,
|
|
114
258
|
method: 'post',
|
|
@@ -118,18 +262,247 @@ class QQBotAPI {
|
|
|
118
262
|
}
|
|
119
263
|
});
|
|
120
264
|
}
|
|
265
|
+
async getMediaFileHashes(filePath) {
|
|
266
|
+
const sha256 = createHash('sha256');
|
|
267
|
+
const md5 = createHash('md5');
|
|
268
|
+
const sha1 = createHash('sha1');
|
|
269
|
+
const md5_10m = createHash('md5');
|
|
270
|
+
let firstBytes = 0;
|
|
271
|
+
const firstLimit = 10_002_432;
|
|
272
|
+
for await (const chunk of createReadStream(filePath)) {
|
|
273
|
+
const data = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
274
|
+
sha256.update(data);
|
|
275
|
+
md5.update(data);
|
|
276
|
+
sha1.update(data);
|
|
277
|
+
if (firstBytes < firstLimit) {
|
|
278
|
+
const length = Math.min(data.length, firstLimit - firstBytes);
|
|
279
|
+
md5_10m.update(data.subarray(0, length));
|
|
280
|
+
firstBytes += length;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return {
|
|
284
|
+
sha256: sha256.digest('hex'),
|
|
285
|
+
md5: md5.digest('hex'),
|
|
286
|
+
sha1: sha1.digest('hex'),
|
|
287
|
+
md5_10m: md5_10m.digest('hex')
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
async postChunkedRichMedia(params) {
|
|
291
|
+
const hasBuffer = Buffer.isBuffer(params.data);
|
|
292
|
+
const hasFile = Boolean(params.filePath);
|
|
293
|
+
if (hasBuffer === hasFile)
|
|
294
|
+
throw new Error('Chunked media requires exactly one of data or filePath');
|
|
295
|
+
const total = hasBuffer ? params.data.length : Number(params.size);
|
|
296
|
+
if (!Number.isSafeInteger(total) || total < 1)
|
|
297
|
+
throw new Error('Chunked media requires a valid file size');
|
|
298
|
+
const hashes = params.hashes || (hasBuffer ? this.#getMediaBufferHashes(params.data) : await this.getMediaFileHashes(params.filePath));
|
|
299
|
+
const prefix = params.scope === 'group' ? `/v2/groups/${params.targetId}` : `/v2/users/${params.targetId}`;
|
|
300
|
+
const prepared = await this.groupService({
|
|
301
|
+
url: `${prefix}/upload_prepare`,
|
|
302
|
+
method: 'post',
|
|
303
|
+
data: {
|
|
304
|
+
file_type: params.fileType,
|
|
305
|
+
file_name: params.name || 'file',
|
|
306
|
+
file_size: total,
|
|
307
|
+
md5: hashes.md5,
|
|
308
|
+
sha1: hashes.sha1,
|
|
309
|
+
md5_10m: hashes.md5_10m
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
let uploaded = 0;
|
|
313
|
+
for (const part of prepared.parts || []) {
|
|
314
|
+
const offset = (part.index - 1) * prepared.block_size;
|
|
315
|
+
const length = Math.min(prepared.block_size, total - offset);
|
|
316
|
+
if (length <= 0)
|
|
317
|
+
throw new Error(`QQ returned invalid upload part index: ${part.index}`);
|
|
318
|
+
const body = hasBuffer ? params.data.subarray(offset, offset + length) : undefined;
|
|
319
|
+
const partMd5 = body ? createHash('md5').update(body).digest('hex') : await this.#getFilePartHash(params.filePath, offset, length);
|
|
320
|
+
await this.#retry(() => axios.put(part.presigned_url, body || createReadStream(params.filePath, { start: offset, end: offset + length - 1 }), {
|
|
321
|
+
timeout: 300_000,
|
|
322
|
+
maxBodyLength: Infinity,
|
|
323
|
+
maxContentLength: Infinity,
|
|
324
|
+
headers: { 'Content-Length': String(length) }
|
|
325
|
+
}));
|
|
326
|
+
await this.#retry(() => this.groupService({
|
|
327
|
+
url: `${prefix}/upload_part_finish`,
|
|
328
|
+
method: 'post',
|
|
329
|
+
data: { upload_id: prepared.upload_id, part_index: part.index, block_size: length, md5: partMd5 }
|
|
330
|
+
}));
|
|
331
|
+
uploaded += length;
|
|
332
|
+
params.onProgress?.(uploaded, total);
|
|
333
|
+
}
|
|
334
|
+
return this.#retry(() => this.groupService({
|
|
335
|
+
url: `${prefix}/complete_upload`,
|
|
336
|
+
method: 'post',
|
|
337
|
+
data: { upload_id: prepared.upload_id }
|
|
338
|
+
}));
|
|
339
|
+
}
|
|
340
|
+
#getMediaBufferHashes(data) {
|
|
341
|
+
return {
|
|
342
|
+
sha256: createHash('sha256').update(data).digest('hex'),
|
|
343
|
+
md5: createHash('md5').update(data).digest('hex'),
|
|
344
|
+
sha1: createHash('sha1').update(data).digest('hex'),
|
|
345
|
+
md5_10m: createHash('md5').update(data.subarray(0, 10_002_432)).digest('hex')
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
async #getFilePartHash(filePath, start, length) {
|
|
349
|
+
const md5 = createHash('md5');
|
|
350
|
+
for await (const chunk of createReadStream(filePath, { start, end: start + length - 1 }))
|
|
351
|
+
md5.update(chunk);
|
|
352
|
+
return md5.digest('hex');
|
|
353
|
+
}
|
|
354
|
+
async #retry(operation, attempts = 3) {
|
|
355
|
+
let lastError;
|
|
356
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
357
|
+
try {
|
|
358
|
+
return await operation();
|
|
359
|
+
}
|
|
360
|
+
catch (error) {
|
|
361
|
+
lastError = error;
|
|
362
|
+
if (attempt + 1 < attempts)
|
|
363
|
+
await new Promise(resolve => setTimeout(resolve, 250 * 2 ** attempt));
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
throw lastError;
|
|
367
|
+
}
|
|
121
368
|
userMessageDelete(openid, message_id) {
|
|
122
369
|
return this.groupService({
|
|
123
370
|
url: `/v2/users/${openid}/messages/${message_id}`,
|
|
124
371
|
method: 'delete'
|
|
125
372
|
});
|
|
126
373
|
}
|
|
127
|
-
|
|
374
|
+
groupMessageDelete(group_openid, message_id) {
|
|
128
375
|
return this.groupService({
|
|
129
376
|
url: `/v2/groups/${group_openid}/messages/${message_id}`,
|
|
130
377
|
method: 'delete'
|
|
131
378
|
});
|
|
132
379
|
}
|
|
380
|
+
grouMessageDelte(group_openid, message_id) {
|
|
381
|
+
return this.groupMessageDelete(group_openid, message_id);
|
|
382
|
+
}
|
|
383
|
+
groupsInfo(group_openid) {
|
|
384
|
+
return this.groupService({
|
|
385
|
+
url: `/v2/groups/${group_openid}/info`,
|
|
386
|
+
method: 'get'
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
groupsBotState(group_openid) {
|
|
390
|
+
return this.groupService({
|
|
391
|
+
url: `/v2/groups/${group_openid}/bot_state`,
|
|
392
|
+
method: 'get'
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
groupsMembersMessage(group_openid, member_openid) {
|
|
396
|
+
return this.groupService({
|
|
397
|
+
url: `/v2/groups/${group_openid}/members/${member_openid}`,
|
|
398
|
+
method: 'get'
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
groupsJoinRequestList(group_openid, params) {
|
|
402
|
+
return this.groupService({
|
|
403
|
+
url: `/v2/groups/${group_openid}/join_request_list`,
|
|
404
|
+
method: 'get',
|
|
405
|
+
params
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
groupsApprovalJoinRequest(group_openid, member_openid, data) {
|
|
409
|
+
return this.groupService({
|
|
410
|
+
url: `/v2/groups/${group_openid}/approval_join_request/${member_openid}`,
|
|
411
|
+
method: 'post',
|
|
412
|
+
data
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
groupsRestrictChatSetting(group_openid) {
|
|
416
|
+
return this.groupService({
|
|
417
|
+
url: `/v2/groups/${group_openid}/restrict_chat_setting`,
|
|
418
|
+
method: 'get'
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
groupsRestrictChatSettingPost(group_openid, data) {
|
|
422
|
+
return this.groupService({
|
|
423
|
+
url: `/v2/groups/${group_openid}/restrict_chat_setting`,
|
|
424
|
+
method: 'post',
|
|
425
|
+
data
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
groupsJoinApprovalStrategies(params) {
|
|
429
|
+
return this.groupService({
|
|
430
|
+
url: '/v2/groups/join_approval_strategy',
|
|
431
|
+
method: 'get',
|
|
432
|
+
params
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
groupsJoinApprovalStrategyCreate(data) {
|
|
436
|
+
return this.groupService({
|
|
437
|
+
url: '/v2/groups/join_approval_strategy',
|
|
438
|
+
method: 'post',
|
|
439
|
+
data
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
groupsJoinApprovalStrategyPatch(strategy_id, data) {
|
|
443
|
+
return this.groupService({
|
|
444
|
+
url: `/v2/groups/join_approval_strategy/${strategy_id}`,
|
|
445
|
+
method: 'patch',
|
|
446
|
+
data
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
groupsJoinApprovalStrategyDelete(strategy_id) {
|
|
450
|
+
return this.groupService({
|
|
451
|
+
url: `/v2/groups/join_approval_strategy/${strategy_id}`,
|
|
452
|
+
method: 'delete'
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
groupsJoinApprovalStrategyExecute(strategy_id) {
|
|
456
|
+
return this.groupService({
|
|
457
|
+
url: `/v2/groups/join_approval_strategy/${strategy_id}/execute`,
|
|
458
|
+
method: 'post'
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
groupsJoinApprovalStrategyWhitelistUsers(strategy_id, data) {
|
|
462
|
+
return this.groupService({
|
|
463
|
+
url: `/v2/groups/join_approval_strategy/${strategy_id}/whitelist_users`,
|
|
464
|
+
method: 'post',
|
|
465
|
+
data
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
usersUploadPrepare(userId, data) {
|
|
469
|
+
return this.groupService({
|
|
470
|
+
url: `/v2/users/${userId}/upload_prepare`,
|
|
471
|
+
method: 'post',
|
|
472
|
+
data
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
groupUploadPrepare(groupId, data) {
|
|
476
|
+
return this.groupService({
|
|
477
|
+
url: `/v2/groups/${groupId}/upload_prepare`,
|
|
478
|
+
method: 'post',
|
|
479
|
+
data
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
usersUploadPartFinish(userId, data) {
|
|
483
|
+
return this.groupService({
|
|
484
|
+
url: `/v2/users/${userId}/upload_part_finish`,
|
|
485
|
+
method: 'post',
|
|
486
|
+
data
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
groupUploadPartFinish(groupId, data) {
|
|
490
|
+
return this.groupService({
|
|
491
|
+
url: `/v2/groups/${groupId}/upload_part_finish`,
|
|
492
|
+
method: 'post',
|
|
493
|
+
data
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
uploadPartDirect(presignedUrl, part) {
|
|
497
|
+
return axios.put(presignedUrl, part);
|
|
498
|
+
}
|
|
499
|
+
streamMessages(userId, data) {
|
|
500
|
+
return this.groupService({
|
|
501
|
+
url: `/v2/users/${userId}/stream_messages`,
|
|
502
|
+
method: 'post',
|
|
503
|
+
data
|
|
504
|
+
});
|
|
505
|
+
}
|
|
133
506
|
async channelsMessages(channel_id, message, image) {
|
|
134
507
|
const formdata = new FormData();
|
|
135
508
|
for (const key in message) {
|