@karinjs/adapter-qqbot 1.0.1

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.
@@ -0,0 +1,434 @@
1
+ import got from 'got';
2
+ import WebSocket from 'ws';
3
+ import { Intents } from './intents.js';
4
+ import { common, logger } from 'node-karin';
5
+ export class QQBotApi {
6
+ /**
7
+ * 开发者的appId
8
+ */
9
+ appId;
10
+ /**
11
+ * Bot的昵称
12
+ */
13
+ nick;
14
+ /**
15
+ * 调用凭证
16
+ */
17
+ #access_token;
18
+ /**
19
+ * wss地址
20
+ */
21
+ wssUrl;
22
+ /**
23
+ * 开发者的Secret 仅在内部使用 防止泄露
24
+ */
25
+ #Secret;
26
+ /**
27
+ * wss连接
28
+ */
29
+ wss;
30
+ seq = 0;
31
+ /**
32
+ * 心跳内容
33
+ */
34
+ heartbeat;
35
+ /**
36
+ * 心跳生命周期 单位:毫秒
37
+ */
38
+ heartbeat_interval;
39
+ /**
40
+ * 请求头 用于鉴权
41
+ */
42
+ headers = {};
43
+ /**
44
+ * 基本请求接口
45
+ */
46
+ host;
47
+ #config;
48
+ constructor(config) {
49
+ this.#config = config;
50
+ this.appId = String(this.#config.appId);
51
+ this.nick = '';
52
+ this.#Secret = this.#config.secret;
53
+ this.host = this.#config.sandBox ? this.#config.sandBoxApi : this.#config.qqBotApi;
54
+ this.#access_token = '';
55
+ this.wssUrl = '';
56
+ this.heartbeat = { op: 1, d: null, session_id: '' };
57
+ this.heartbeat_interval = 0;
58
+ // 后续处理 重启后重新建立历史会话
59
+ }
60
+ logger(level, ...args) {
61
+ logger.bot(level, this.appId, ...args);
62
+ }
63
+ /**
64
+ * post请求
65
+ * @param url 请求地址
66
+ * @param options 请求参数
67
+ */
68
+ async post(url, options) {
69
+ try {
70
+ return await got.post(url, { json: options, headers: this.headers });
71
+ }
72
+ catch (error) {
73
+ logger.error(`[got] 请求错误: ${JSON.stringify(error.response.body)}`);
74
+ }
75
+ }
76
+ /**
77
+ * 获取调用凭证
78
+ * @param clientSecret 开发者的Secret
79
+ * @param isRefresh 是否是刷新凭证
80
+ */
81
+ async getAccessToken(isRefresh = false) {
82
+ if (isRefresh)
83
+ this.logger('debug', '凭证即将过期,开始刷新凭证');
84
+ const url = this.#config.accessTokenApi;
85
+ const data = await got.post(url, {
86
+ json: { appId: this.appId, clientSecret: this.#Secret },
87
+ headers: { 'Content-Type': 'application/json' },
88
+ }).json();
89
+ const { expires_in, access_token } = data;
90
+ this.#access_token = access_token;
91
+ this.headers = { Authorization: `QQBot ${this.#access_token}`, 'X-Union-Appid': `BOT_${this.appId}` };
92
+ this.logger('debug', `凭证获取成功: ${JSON.stringify(data)}`);
93
+ /** 刷新凭证 */
94
+ const time = Number(expires_in) * 1000 - 50000;
95
+ setTimeout(async () => {
96
+ this.getAccessToken(true);
97
+ /** 重新鉴权 */
98
+ }, time);
99
+ return data;
100
+ }
101
+ /**
102
+ * wss鉴权
103
+ * @param isAuth 是否鉴权
104
+ */
105
+ async wssAuth(isAuth) {
106
+ if (!isAuth) {
107
+ /** 断连后不需要重新ws鉴权 需要恢复登录态 Session */
108
+ const data = {
109
+ op: 6 /* Opcode.Resume */,
110
+ d: {
111
+ token: `QQBot ${this.#access_token}`,
112
+ session_id: this.heartbeat.session_id,
113
+ seq: this.heartbeat.d || 0,
114
+ },
115
+ };
116
+ this.wss.send(JSON.stringify(data));
117
+ return;
118
+ }
119
+ const intents = Intents(['GROUP_AT_MESSAGE_CREATE']);
120
+ if (intents === 0)
121
+ throw new Error('intents 无效,请检查传入的事件名称是否正确');
122
+ const data = {
123
+ op: 2 /* Opcode.Identify */,
124
+ d: {
125
+ token: `QQBot ${this.#access_token}`,
126
+ intents,
127
+ shard: [0, 1],
128
+ properties: {
129
+ $os: 'Linux',
130
+ $browser: 'karin-plugin-adapter-qqbot',
131
+ $device: 'karin',
132
+ },
133
+ },
134
+ };
135
+ this.wss.send(JSON.stringify(data));
136
+ }
137
+ /**
138
+ * 获取wss地址
139
+ */
140
+ async getWssUrl() {
141
+ const url = `${this.host}/gateway`;
142
+ const data = await got.get(url, { headers: this.headers }).json();
143
+ this.wssUrl = data.url;
144
+ }
145
+ /**
146
+ * 创建wss连接
147
+ */
148
+ createWss(isAuth = true) {
149
+ this.wss = new WebSocket(this.wssUrl);
150
+ /** 建立连接成功之后 需要鉴权 */
151
+ this.wss.once('open', () => this.wssAuth(isAuth));
152
+ /** 监听事件 */
153
+ this.wss.on('message', (data) => this.payload(data));
154
+ /** 监听ws关闭 */
155
+ this.wss.once('close', () => {
156
+ this.logger('error', 'ws连接关闭,正在重连');
157
+ /** 回收监听器 */
158
+ this.wss.removeAllListeners();
159
+ this.createWss(false);
160
+ });
161
+ }
162
+ payload(payload) {
163
+ const data = JSON.parse(payload);
164
+ this.logger('debug', `收到事件推送: ${JSON.stringify(data, null, 2)}`);
165
+ switch (data.op) {
166
+ case 0:
167
+ this.intents(data);
168
+ this.logger('debug', '事件推送', JSON.stringify(data, null, 2));
169
+ break;
170
+ case 1 /* Opcode.Heartbeat */:
171
+ /** 在指定时间进行回包 */
172
+ this.wss.send(JSON.stringify({ op: 11, d: null }));
173
+ this.logger('debug', '心跳', JSON.stringify(data, null, 2));
174
+ break;
175
+ case 2 /* Opcode.Identify */:
176
+ this.logger('info', '鉴权成功', JSON.stringify(data, null, 2));
177
+ break;
178
+ case 6 /* Opcode.Resume */:
179
+ this.logger('info', '客户端恢复连接', JSON.stringify(data, null, 2));
180
+ break;
181
+ case 7 /* Opcode.Reconnect */:
182
+ this.logger('info', '服务端要求重新连接', JSON.stringify(data, null, 2));
183
+ /** 断开已有ws */
184
+ this.wss.close();
185
+ break;
186
+ case 9 /* Opcode.InvalidSession */:
187
+ /** 当 identify 或 resume 的时候,如果参数有错,服务端会返回该消息 */
188
+ this.logger('error', '参数错误', JSON.stringify(data, null, 2));
189
+ break;
190
+ case 10 /* Opcode.Hello */:
191
+ /** 更新生命周期 对声明周期减少3秒 防止过时 */
192
+ this.heartbeat_interval = data.d.heartbeat_interval - 3000;
193
+ this.logger('debug', '心跳生命周期更新', JSON.stringify(data, null, 2));
194
+ break;
195
+ case 11 /* Opcode.HeartbeatACK */:
196
+ this.logger('debug', '心跳回包', JSON.stringify(data, null, 2));
197
+ break;
198
+ case 12 /* Opcode.HTTPCallbackACK */:
199
+ /** 仅用于 http 回调模式的回包,代表机器人收到了平台推送的数据 */
200
+ this.logger('debug', '回包', JSON.stringify(data, null, 2));
201
+ break;
202
+ }
203
+ }
204
+ /**
205
+ * 事件处理
206
+ */
207
+ async intents(data) {
208
+ switch (data.t) {
209
+ case "READY" /* EventType.READY */: {
210
+ /** 发送第一次心跳 */
211
+ this.wss.send(JSON.stringify(this.heartbeat));
212
+ /** 更新心跳唯一值 */
213
+ this.heartbeat.d = data.s;
214
+ /** 记录session_id 用于重新连接 */
215
+ this.heartbeat.session_id = data.d.session_id;
216
+ /** 心跳 */
217
+ setInterval(() => this.wss.send(JSON.stringify(this.heartbeat)), this.heartbeat_interval);
218
+ this.nick = data.d.user.username;
219
+ this.wss.emit('start');
220
+ break;
221
+ }
222
+ case "GROUP_AT_MESSAGE_CREATE" /* EventType.GROUP_AT_MESSAGE_CREATE */: {
223
+ this.heartbeat.d = data.s;
224
+ this.wss.emit("GROUP_AT_MESSAGE_CREATE" /* EventType.GROUP_AT_MESSAGE_CREATE */, data);
225
+ break;
226
+ }
227
+ case "C2C_MESSAGE_CREATE" /* EventType.C2C_MESSAGE_CREATE */: {
228
+ this.heartbeat.d = data.s;
229
+ this.wss.emit("C2C_MESSAGE_CREATE" /* EventType.C2C_MESSAGE_CREATE */, data);
230
+ break;
231
+ }
232
+ // case 'GUILD_CREATE':
233
+ // case 'GUILD_UPDATE':
234
+ // case 'GUILD_DELETE':
235
+ // case 'CHANNEL_CREATE':
236
+ // case 'CHANNEL_UPDATE':
237
+ // case 'CHANNEL_DELETE':
238
+ // case 'GUILD_MEMBER_ADD':
239
+ // case 'GUILD_MEMBER_UPDATE':
240
+ // case 'GUILD_MEMBER_REMOVE':
241
+ // case 'MESSAGE_CREATE':
242
+ // case 'MESSAGE_DELETE':
243
+ // case 'MESSAGE_REACTION_ADD':
244
+ // case 'MESSAGE_REACTION_REMOVE':
245
+ // case 'DIRECT_MESSAGE_CREATE':
246
+ // case 'DIRECT_MESSAGE_DELETE':
247
+ // case 'FRIEND_ADD':
248
+ // case 'FRIEND_DEL':
249
+ // case 'C2C_MSG_REJECT':
250
+ // case 'C2C_MSG_RECEIVE':
251
+ // case 'GROUP_ADD_ROBOT':
252
+ // case 'GROUP_DEL_ROBOT':
253
+ // case 'GROUP_MSG_REJECT':
254
+ // case 'GROUP_MSG_RECEIVE':
255
+ // case 'INTERACTION_CREATE':
256
+ // case 'MESSAGE_AUDIT_PASS':
257
+ // case 'MESSAGE_AUDIT_REJECT':
258
+ // case 'FORUM_THREAD_CREATE':
259
+ // case 'FORUM_THREAD_UPDATE':
260
+ // case 'FORUM_THREAD_DELETE':
261
+ // case 'FORUM_POST_CREATE':
262
+ // case 'FORUM_POST_DELETE':
263
+ // case 'FORUM_REPLY_CREATE':
264
+ // case 'FORUM_REPLY_DELETE':
265
+ // case 'FORUM_PUBLISH_AUDIT_RESULT':
266
+ // case 'AUDIO_START':
267
+ // case 'AUDIO_FINISH':
268
+ // case 'AUDIO_ON_MIC':
269
+ // case 'AUDIO_OFF_MIC':
270
+ // case 'AT_MESSAGE_CREATE':
271
+ // case 'PUBLIC_MESSAGE_DELETE':
272
+ // case 'RESUMED: {"op":0,"s":53,"t":"RESUMED","d":""} 在客户端恢复连接时收到
273
+ default:
274
+ console.log(`Unhandled event: ${JSON.stringify(data)}`);
275
+ }
276
+ }
277
+ /**
278
+ * 发送消息
279
+ * @param openid 用户、群openid
280
+ * @param type 请求路径类型
281
+ * @param options 请求参数
282
+ */
283
+ async sendMessage(openid, type, options) {
284
+ const url = this.buildUrl(openid, type, "messages" /* V2ApiType.Message */);
285
+ const result = await this.post(url, options);
286
+ return JSON.parse(result.body);
287
+ }
288
+ /**
289
+ * 构建发送文本消息请求参数
290
+ */
291
+ buildText(content, id, seq) {
292
+ const options = {
293
+ content,
294
+ msg_type: 0 /* MessageType.Text */,
295
+ };
296
+ /** id存在 */
297
+ if (id) {
298
+ const { key, value } = this.buildId(id);
299
+ options[key] = value;
300
+ options["msg_seq" /* SeqType.MsgSeq */] = seq;
301
+ }
302
+ return options;
303
+ }
304
+ /**
305
+ * 上传富媒体文件
306
+ * @param openid 用户、群openid
307
+ * @param type 请求类型
308
+ * @param file 文件内容 http、base64、file://
309
+ * @param file_type 文件类型
310
+ */
311
+ async uploadMedia(openid, type, file, file_type) {
312
+ const options = await this.buildUploadMedia(file, file_type);
313
+ const url = this.buildUrl(openid, type, "files" /* V2ApiType.Files */);
314
+ return await got.post(url, options).json();
315
+ }
316
+ /**
317
+ * 构建v2接口请求地址
318
+ * @param openid 用户、群openid
319
+ * @param type 请求类型
320
+ * @param apiType v2接口类型
321
+ */
322
+ buildUrl(openid, type, apiType) {
323
+ return `${this.host}/v2/${type}/${openid}/${apiType}`;
324
+ }
325
+ /**
326
+ * 传入一个msg_id或者event_id 返回其对应的key和value
327
+ * @param id 消息id或者事件id
328
+ */
329
+ buildId(id) {
330
+ if (id.startsWith(':')) {
331
+ return { key: "event_id" /* IdType.EventID */, value: id };
332
+ }
333
+ else {
334
+ return { key: "msg_id" /* IdType.MsgID */, value: id };
335
+ }
336
+ }
337
+ /**
338
+ * 构建发送富媒体请求参数
339
+ * 富媒体消息只能单独发送 并且必须进行上传
340
+ * @param file_info 富媒体接口返回的file_info
341
+ * @param id 消息id或者事件id
342
+ */
343
+ buildMedia(file_info, id, seq) {
344
+ const options = {
345
+ content: '',
346
+ msg_type: 7 /* MessageType.Media */,
347
+ media: {
348
+ file_info,
349
+ },
350
+ };
351
+ /** id存在 */
352
+ if (id) {
353
+ const { key, value } = this.buildId(id);
354
+ options[key] = value;
355
+ options["msg_seq" /* SeqType.MsgSeq */] = seq;
356
+ }
357
+ return options;
358
+ }
359
+ /**
360
+ * 构建发送Markdown模板请求参数
361
+ * @param custom_template_id 模板id
362
+ * @param params 模板参数
363
+ * @param id 消息id或者事件id
364
+ * @param seq 消息序号
365
+ */
366
+ buildTplMarkdown(custom_template_id, params, keyboard, id, seq) {
367
+ const options = {
368
+ msg_type: 2 /* MessageType.Markdown */,
369
+ content: '',
370
+ keyboard,
371
+ markdown: {
372
+ custom_template_id,
373
+ params,
374
+ },
375
+ };
376
+ /** id存在 */
377
+ if (id) {
378
+ const { key, value } = this.buildId(id);
379
+ options[key] = value;
380
+ options["msg_seq" /* SeqType.MsgSeq */] = seq;
381
+ }
382
+ return options;
383
+ }
384
+ /**
385
+ * 构建发送原生Markdown请求参数
386
+ * @param content markdown文本
387
+ * @param id 消息id或者事件id
388
+ * @param seq 消息序号
389
+ */
390
+ buildRawMarkdown(content, keyboard, id, seq) {
391
+ const options = {
392
+ msg_type: 2 /* MessageType.Markdown */,
393
+ content: '',
394
+ markdown: { content },
395
+ keyboard,
396
+ };
397
+ /** id存在 */
398
+ if (id) {
399
+ const { key, value } = this.buildId(id);
400
+ options[key] = value;
401
+ options["msg_seq" /* SeqType.MsgSeq */] = seq;
402
+ }
403
+ return options;
404
+ }
405
+ /**
406
+ * 构建上传富媒体请求参数
407
+ * @param file 文件内容 http、base64、file://
408
+ * @param file_type 文件类型
409
+ */
410
+ async buildUploadMedia(file, file_type) {
411
+ /** 非http转base64 */
412
+ if (!file.startsWith('http')) {
413
+ file = await common.base64(file);
414
+ return {
415
+ json: {
416
+ /** 发base64,无前缀 */
417
+ file_data: file,
418
+ file_type,
419
+ srv_send_msg: false,
420
+ },
421
+ headers: this.headers,
422
+ };
423
+ }
424
+ return {
425
+ json: {
426
+ /** http */
427
+ url: file,
428
+ file_type,
429
+ srv_send_msg: false,
430
+ },
431
+ headers: this.headers,
432
+ };
433
+ }
434
+ }
@@ -0,0 +1,92 @@
1
+ import { QQBotApi } from './api.js';
2
+ import { PathType, C2CMessageCreateEvent, GroupAtMessageCreateEvent } from '../types/index.js';
3
+ import { GlobalCfgType, AccountCfgType } from '../utils/index.js';
4
+ import { Contact, LoggerLevel, ReplyReturn, KarinAdapter, KarinElement } from 'node-karin';
5
+ /**
6
+ * - QQBot适配器
7
+ */
8
+ export declare class AdapterQQBot implements KarinAdapter {
9
+ #private;
10
+ super: QQBotApi;
11
+ socket: WebSocket;
12
+ account: KarinAdapter['account'];
13
+ adapter: KarinAdapter['adapter'];
14
+ version: KarinAdapter['version'];
15
+ markdown: GlobalCfgType;
16
+ constructor(config: AccountCfgType);
17
+ init(): Promise<void>;
18
+ _sendNsg(elements: Array<KarinElement>, type: PathType, openid: string, message_id?: string): Promise<ReplyReturn>;
19
+ get self_id(): string;
20
+ logger(level: LoggerLevel, ...args: any[]): void;
21
+ GetVersion(): Promise<{
22
+ name: string;
23
+ app_name: string;
24
+ version: string;
25
+ }>;
26
+ /**
27
+ * QQBot转karin
28
+ * @return karin格式消息
29
+ * */
30
+ AdapterConvertKarin(event: C2CMessageCreateEvent | GroupAtMessageCreateEvent): Array<KarinElement>;
31
+ /**
32
+ * karin转qqbot
33
+ * @param data karin格式消息
34
+ * */
35
+ KarinConvertAdapter(data: Array<KarinElement>, type: PathType, openid: string, message_id?: string): Promise<ReplyReturn>;
36
+ SendMessage(_contact: Contact, elements: Array<KarinElement>): Promise<{
37
+ message_id: string;
38
+ }>;
39
+ getAvatarUrl(): string;
40
+ getGroupAvatar(): string;
41
+ GetCurrentAccount(): Promise<{
42
+ account_uid: string;
43
+ account_uin: string;
44
+ account_name: string;
45
+ }>;
46
+ GetEssenceMessageList(): Promise<any>;
47
+ DownloadForwardMessage(): Promise<any>;
48
+ SetEssenceMessage(): Promise<any>;
49
+ DeleteEssenceMessage(): Promise<any>;
50
+ SetFriendApplyResult(): Promise<any>;
51
+ SetGroupApplyResult(): Promise<any>;
52
+ SetInvitedJoinGroupResult(): Promise<any>;
53
+ ReactMessageWithEmoji(): Promise<any>;
54
+ UploadPrivateFile(): Promise<any>;
55
+ UploadGroupFile(): Promise<any>;
56
+ UploadForwardMessage(): Promise<any>;
57
+ sendForwardMessage(): Promise<any>;
58
+ SendMessageByResId(): Promise<any>;
59
+ RecallMessage(): Promise<any>;
60
+ GetMessage(): Promise<any>;
61
+ GetHistoryMessage(): Promise<any>;
62
+ VoteUser(): Promise<any>;
63
+ KickMember(): Promise<any>;
64
+ BanMember(): Promise<any>;
65
+ SetGroupWholeBan(): Promise<any>;
66
+ SetGroupAdmin(): Promise<any>;
67
+ ModifyMemberCard(): Promise<any>;
68
+ ModifyGroupName(): Promise<any>;
69
+ LeaveGroup(): Promise<any>;
70
+ SetGroupUniqueTitle(): Promise<any>;
71
+ GetStrangerProfileCard(): Promise<any>;
72
+ GetFriendList(): Promise<any>;
73
+ GetGroupInfo(): Promise<any>;
74
+ GetGroupList(): Promise<any>;
75
+ GetGroupMemberInfo(): Promise<any>;
76
+ GetGroupMemberList(): Promise<any>;
77
+ GetGroupHonor(): Promise<any>;
78
+ DownloadFile(): Promise<any>;
79
+ CreateFolder(): Promise<any>;
80
+ RenameFolder(): Promise<any>;
81
+ DeleteFolde(): Promise<any>;
82
+ DeleteFolder(): Promise<any>;
83
+ UploadFile(): Promise<any>;
84
+ DeleteFile(): Promise<any>;
85
+ GetFileSystemInfo(): Promise<any>;
86
+ GetFileList(): Promise<any>;
87
+ ModifyGroupRemark(): Promise<any>;
88
+ GetRemainCountAtAll(): Promise<any>;
89
+ GetProhibitedUserList(): Promise<any>;
90
+ PokeMember(): Promise<any>;
91
+ SetMessageReaded(): Promise<any>;
92
+ }