@alemonjs/qq-bot 0.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.
package/lib/client.js ADDED
@@ -0,0 +1,176 @@
1
+ import { QQBotAPI } from './api.js';
2
+ import bodyParser from 'koa-bodyparser';
3
+ import Router from 'koa-router';
4
+ import { WebhookAPI } from './webhook.js';
5
+ import Koa from 'koa';
6
+ import { config } from './config.js';
7
+ import { v4 } from 'uuid';
8
+ import { WebSocketServer, WebSocket } from 'ws';
9
+
10
+ class QQBotClient extends QQBotAPI {
11
+ #events = {};
12
+ #app = null;
13
+ #count = 0;
14
+ #client = [];
15
+ #ws = null;
16
+ /**
17
+ * 设置配置
18
+ * @param opstion
19
+ */
20
+ constructor(opstion) {
21
+ super();
22
+ config.set('secret', opstion.secret);
23
+ config.set('appId', opstion.appId);
24
+ config.set('token', opstion.token);
25
+ config.set('port', opstion.port);
26
+ config.set('ws', opstion.ws);
27
+ }
28
+ /**
29
+ * 注册事件处理程序
30
+ * @param key 事件名称
31
+ * @param val 事件处理函数
32
+ */
33
+ on(key, val) {
34
+ if (!this.#events[key]) {
35
+ this.#events[key] = [];
36
+ }
37
+ this.#events[key].push(val);
38
+ return this;
39
+ }
40
+ /**
41
+ *
42
+ * @param cfg
43
+ * @param conversation
44
+ */
45
+ async connect() {
46
+ this.#app = new Koa();
47
+ this.#app.use(bodyParser());
48
+ const router = new Router();
49
+ const port = config.get('port');
50
+ const cfg = {
51
+ secret: config.get('secret'),
52
+ port: port ? Number(port) : 17157
53
+ };
54
+ const ntqqWebhook = new WebhookAPI({
55
+ secret: 'YI2mWG0lWH2nYJ4qcOAwiUH4reRE1pdR'
56
+ });
57
+ this.#app.use(async (ctx, next) => {
58
+ let rawData = '';
59
+ ctx.req.on('data', chunk => (rawData += chunk));
60
+ ctx.req.on('end', () => (ctx.request.rawBody = rawData));
61
+ await next();
62
+ });
63
+ // 启动服务
64
+ router.post('/webhook', async (ctx) => {
65
+ const sign = ctx.req.headers['x-signature-ed25519'];
66
+ const timestamp = ctx.req.headers['x-signature-timestamp'];
67
+ const rawBody = ctx.request.rawBody;
68
+ const isValid = ntqqWebhook.validSign(timestamp, rawBody, String(sign));
69
+ if (!isValid) {
70
+ ctx.status = 400;
71
+ ctx.body = { msg: 'invalid signature' };
72
+ return;
73
+ }
74
+ const body = ctx.request.body;
75
+ if (body.op == 13) {
76
+ ctx.status = 200;
77
+ ctx.body = {
78
+ // 返回明文 token
79
+ plain_token: body.d.plain_token,
80
+ // 生成签名
81
+ signature: ntqqWebhook.getSign(body.d.event_ts, body.d.plain_token)
82
+ };
83
+ }
84
+ else if (body.op == 0) {
85
+ ctx.status = 204;
86
+ console.log('body', body.d);
87
+ // 根据事件类型,处理事件
88
+ for (const event of this.#events[body.t] || []) {
89
+ event(body.d);
90
+ }
91
+ // 也可以分法到客户端。 发送失败需要处理 或清理调
92
+ for (const client of this.#client) {
93
+ try {
94
+ client.ws.send(JSON.stringify(body.d));
95
+ }
96
+ catch (error) {
97
+ console.error('send error', error);
98
+ }
99
+ }
100
+ }
101
+ });
102
+ this.#app.use(router.routes());
103
+ this.#app.use(router.allowedMethods());
104
+ // 启动服务
105
+ const server = this.#app.listen(cfg.port, () => {
106
+ console.log('Server running at http://localhost:' + cfg.port + '/webhook');
107
+ });
108
+ // 创建 WebSocketServer 并监听同一个端口
109
+ const wss = new WebSocketServer({ server: server });
110
+ // 处理客户端连接
111
+ wss.on('connection', ws => {
112
+ const clientId = v4();
113
+ ws['clientId'] = clientId;
114
+ console.log(clientId, 'connection');
115
+ this.#client.push({ id: clientId, ws });
116
+ // 处理消息事件
117
+ ws.on('message', (message) => {
118
+ // 拿到消息
119
+ try {
120
+ const body = JSON.parse(message.toString());
121
+ for (const event of this.#events[body.t] || []) {
122
+ event(body);
123
+ }
124
+ }
125
+ catch (e) {
126
+ console.error('parse error', e);
127
+ }
128
+ });
129
+ // 处理关闭事件
130
+ ws.on('close', () => {
131
+ console.log(`Client ${clientId} disconnected`);
132
+ this.#client = this.#client.filter(client => client.id !== clientId);
133
+ });
134
+ });
135
+ const reconnect = () => {
136
+ const ws = config.get('ws');
137
+ if (!ws)
138
+ return;
139
+ // 使用了ws服务器
140
+ this.#ws = new WebSocket(ws);
141
+ this.#ws.on('open', () => {
142
+ this.#count = 0;
143
+ console.log('ws connected');
144
+ });
145
+ this.#ws.on('message', data => {
146
+ try {
147
+ // 拿到消息
148
+ const body = JSON.parse(data.toString());
149
+ for (const event of this.#events[body.t] || []) {
150
+ event(body);
151
+ }
152
+ }
153
+ catch (e) {
154
+ console.error('parse error', e);
155
+ }
156
+ });
157
+ this.#ws.on('close', () => {
158
+ console.log('ws closed');
159
+ // 重连5次,超过5次不再重连
160
+ if (this.#count > 5) {
161
+ return;
162
+ }
163
+ // 23s 后重连
164
+ setTimeout(() => {
165
+ reconnect();
166
+ }, 23000);
167
+ });
168
+ this.#ws.on('error', () => {
169
+ console.log('ws error ');
170
+ });
171
+ };
172
+ reconnect();
173
+ }
174
+ }
175
+
176
+ export { QQBotClient };
package/lib/config.js ADDED
@@ -0,0 +1,3 @@
1
+ const config = new Map();
2
+
3
+ export { config };
package/lib/from.js ADDED
@@ -0,0 +1,44 @@
1
+ import { existsSync, createReadStream } from 'fs';
2
+ import { Readable, isReadable } from 'stream';
3
+ import { basename } from 'path';
4
+ import { fileTypeFromBuffer, fileTypeFromStream } from 'file-type';
5
+
6
+ /**
7
+ * 创建form
8
+ * @param image
9
+ * @param name
10
+ * @returns
11
+ */
12
+ async function createPicFrom(image, name = 'image.jpg') {
13
+ let picData;
14
+ // 是 string
15
+ if (typeof image === 'string') {
16
+ if (!existsSync(image))
17
+ return false;
18
+ if (!name)
19
+ name = basename(image);
20
+ picData = createReadStream(image);
21
+ // 是 buffer
22
+ }
23
+ else if (Buffer.isBuffer(image)) {
24
+ const { ext } = await fileTypeFromBuffer(image);
25
+ if (!name)
26
+ name = 'file.' + ext;
27
+ picData = new Readable();
28
+ picData.push(image);
29
+ picData.push(null);
30
+ // 是 文件流
31
+ }
32
+ else if (isReadable(image)) {
33
+ const { ext } = await fileTypeFromStream(image);
34
+ if (!name)
35
+ name = 'file.' + ext;
36
+ picData = image;
37
+ }
38
+ else {
39
+ return false;
40
+ }
41
+ return { picData, image, name };
42
+ }
43
+
44
+ export { createPicFrom };
package/lib/index.js ADDED
@@ -0,0 +1,396 @@
1
+ import { defineBot, getConfig, useUserHashKey, OnProcessor } from 'alemonjs';
2
+ import { QQBotClient } from './client.js';
3
+
4
+ const platform = 'qq-bot';
5
+ var index = defineBot(() => {
6
+ const cfg = getConfig();
7
+ const config = cfg.value['qq-bot'];
8
+ if (!config)
9
+ return;
10
+ const client = new QQBotClient({
11
+ secret: config.secret,
12
+ appId: config.appId,
13
+ token: config.token,
14
+ port: config.port,
15
+ ws: config?.ws
16
+ });
17
+ // 连接
18
+ client.connect();
19
+ // 监听消息
20
+ client.on('GROUP_AT_MESSAGE_CREATE', async (event) => {
21
+ const master_key = config?.master_key ?? [];
22
+ const isMaster = master_key.includes(event.author.id);
23
+ const url = `https://q.qlogo.cn/qqapp/${config.app_id}/${event.author.id}/640`;
24
+ const UserAvatar = {
25
+ toBuffer: async () => {
26
+ const arrayBuffer = await fetch(url).then(res => res.arrayBuffer());
27
+ return Buffer.from(arrayBuffer);
28
+ },
29
+ toBase64: async () => {
30
+ const arrayBuffer = await fetch(url).then(res => res.arrayBuffer());
31
+ return Buffer.from(arrayBuffer).toString('base64');
32
+ },
33
+ toURL: async () => {
34
+ return url;
35
+ }
36
+ };
37
+ const UserId = event.author.id;
38
+ const UserKey = useUserHashKey({
39
+ Platform: platform,
40
+ UserId: UserId
41
+ });
42
+ // 定义消
43
+ const e = {
44
+ // 事件类型
45
+ Platform: platform,
46
+ // guild
47
+ GuildId: event.group_id,
48
+ ChannelId: event.group_id,
49
+ // 用户Id
50
+ UserId: event.author.id,
51
+ UserKey,
52
+ UserAvatar: UserAvatar,
53
+ IsMaster: isMaster,
54
+ IsBot: false,
55
+ // 格式化数据
56
+ MessageId: event.id,
57
+ MessageText: event.content?.trim(),
58
+ OpenId: event.author.member_openid,
59
+ CreateAt: Date.now(),
60
+ //
61
+ tag: 'GROUP_AT_MESSAGE_CREATE',
62
+ value: null
63
+ };
64
+ // 当访问的时候获取
65
+ Object.defineProperty(e, 'value', {
66
+ get() {
67
+ return event;
68
+ }
69
+ });
70
+ // 处理消息
71
+ OnProcessor(e, 'message.create');
72
+ });
73
+ client.on('C2C_MESSAGE_CREATE', async (event) => {
74
+ const master_key = config?.master_key ?? [];
75
+ const isMaster = master_key.includes(event.author.id);
76
+ const url = `https://q.qlogo.cn/qqapp/${config.app_id}/${event.author.id}/640`;
77
+ const UserAvatar = {
78
+ toBuffer: async () => {
79
+ const arrayBuffer = await fetch(url).then(res => res.arrayBuffer());
80
+ return Buffer.from(arrayBuffer);
81
+ },
82
+ toBase64: async () => {
83
+ const arrayBuffer = await fetch(url).then(res => res.arrayBuffer());
84
+ return Buffer.from(arrayBuffer).toString('base64');
85
+ },
86
+ toURL: async () => {
87
+ return url;
88
+ }
89
+ };
90
+ const UserId = event.author.id;
91
+ const UserKey = useUserHashKey({
92
+ Platform: platform,
93
+ UserId: UserId
94
+ });
95
+ // 定义消
96
+ const e = {
97
+ // 事件类型
98
+ Platform: platform,
99
+ // 用户Id
100
+ UserId: event.author.id,
101
+ UserKey,
102
+ UserAvatar: UserAvatar,
103
+ IsMaster: isMaster,
104
+ IsBot: false,
105
+ // 格式化数据
106
+ MessageId: event.id,
107
+ MessageText: event.content?.trim(),
108
+ CreateAt: Date.now(),
109
+ OpenId: '',
110
+ //
111
+ tag: 'GROUP_AT_MESSAGE_CREATE',
112
+ value: null
113
+ };
114
+ // 当访问的时候获取
115
+ Object.defineProperty(e, 'value', {
116
+ get() {
117
+ return event;
118
+ }
119
+ });
120
+ // 处理消息
121
+ OnProcessor(e, 'private.message.create');
122
+ });
123
+ client.on('DIRECT_MESSAGE_CREATE', async (event) => {
124
+ // 屏蔽其他机器人的消息
125
+ if (event?.author?.bot)
126
+ return;
127
+ const master_key = config?.master_key ?? [];
128
+ const isMaster = master_key.includes(event.author.id);
129
+ let msg = event?.content ?? '';
130
+ const UserAvatar = {
131
+ toBuffer: async () => {
132
+ const arrayBuffer = await fetch(event.author.avatar).then(res => res.arrayBuffer());
133
+ return Buffer.from(arrayBuffer);
134
+ },
135
+ toBase64: async () => {
136
+ const arrayBuffer = await fetch(event?.author?.avatar).then(res => res.arrayBuffer());
137
+ return Buffer.from(arrayBuffer).toString('base64');
138
+ },
139
+ toURL: async () => {
140
+ return event?.author?.avatar;
141
+ }
142
+ };
143
+ const UserId = event.author.id;
144
+ const UserKey = useUserHashKey({
145
+ Platform: platform,
146
+ UserId: UserId
147
+ });
148
+ // 定义消
149
+ const e = {
150
+ // 事件类型
151
+ Platform: platform,
152
+ //
153
+ GuildId: event.guild_id,
154
+ ChannelId: event.channel_id,
155
+ // 用户Id
156
+ UserId: event?.author?.id ?? '',
157
+ UserKey,
158
+ UserName: event?.author?.username ?? '',
159
+ UserAvatar: UserAvatar,
160
+ IsMaster: isMaster,
161
+ IsBot: event.author?.bot,
162
+ // message
163
+ MessageId: event.id,
164
+ MessageText: msg,
165
+ OpenId: event.guild_id,
166
+ CreateAt: Date.now(),
167
+ //
168
+ tag: 'AT_MESSAGE_CREATE',
169
+ //
170
+ value: null
171
+ };
172
+ // 当访问的时候获取
173
+ Object.defineProperty(e, 'value', {
174
+ get() {
175
+ return event;
176
+ }
177
+ });
178
+ // 处理消息
179
+ OnProcessor(e, 'private.message.create');
180
+ });
181
+ // 监听消息
182
+ client.on('AT_MESSAGE_CREATE', async (event) => {
183
+ // 屏蔽其他机器人的消息
184
+ if (event?.author?.bot)
185
+ return;
186
+ const master_key = config?.master_key ?? [];
187
+ const isMaster = master_key.includes(event.author.id);
188
+ let msg = getMessageContent(event);
189
+ const UserAvatar = {
190
+ toBuffer: async () => {
191
+ const arrayBuffer = await fetch(event.author.avatar).then(res => res.arrayBuffer());
192
+ return Buffer.from(arrayBuffer);
193
+ },
194
+ toBase64: async () => {
195
+ const arrayBuffer = await fetch(event?.author?.avatar).then(res => res.arrayBuffer());
196
+ return Buffer.from(arrayBuffer).toString('base64');
197
+ },
198
+ toURL: async () => {
199
+ return event?.author?.avatar;
200
+ }
201
+ };
202
+ const UserId = event.author.id;
203
+ const UserKey = useUserHashKey({
204
+ Platform: platform,
205
+ UserId: UserId
206
+ });
207
+ // 定义消
208
+ const e = {
209
+ // 事件类型
210
+ Platform: platform,
211
+ GuildId: event.guild_id,
212
+ ChannelId: event.channel_id,
213
+ IsMaster: isMaster,
214
+ // 用户Id
215
+ UserId: event?.author?.id ?? '',
216
+ UserKey,
217
+ UserName: event?.author?.username ?? '',
218
+ UserAvatar: UserAvatar,
219
+ IsBot: event.author?.bot,
220
+ // message
221
+ MessageId: event.id,
222
+ MessageText: msg,
223
+ OpenId: event.guild_id,
224
+ CreateAt: Date.now(),
225
+ //
226
+ tag: 'AT_MESSAGE_CREATE',
227
+ //
228
+ value: null
229
+ };
230
+ // 当访问的时候获取
231
+ Object.defineProperty(e, 'value', {
232
+ get() {
233
+ return event;
234
+ }
235
+ });
236
+ // 处理消息
237
+ OnProcessor(e, 'message.create');
238
+ });
239
+ /**
240
+ *
241
+ * @param event
242
+ * @returns
243
+ */
244
+ const getMessageContent = event => {
245
+ let msg = event?.content ?? '';
246
+ // 艾特消息处理
247
+ const at_users = [];
248
+ if (event.mentions) {
249
+ // 去掉@ 转为纯消息
250
+ for (const item of event.mentions) {
251
+ at_users.push({
252
+ id: item.id
253
+ });
254
+ }
255
+ // 循环删除文本中的at信息并去除前后空格
256
+ at_users.forEach(item => {
257
+ msg = msg.replace(`<@!${item.id}>`, '').trim();
258
+ });
259
+ }
260
+ return msg;
261
+ };
262
+ // 私域 -
263
+ client.on('MESSAGE_CREATE', async (event) => {
264
+ // 屏蔽其他机器人的消息
265
+ if (event.author?.bot)
266
+ return;
267
+ // 撤回消息
268
+ if (new RegExp(/DELETE$/).test(event.eventType))
269
+ return;
270
+ const master_key = config?.master_key ?? [];
271
+ const UserId = event.author.id;
272
+ const isMaster = master_key.includes(UserId);
273
+ const msg = getMessageContent(event);
274
+ const UserAvatar = {
275
+ toBuffer: async () => {
276
+ const arrayBuffer = await fetch(event.author.avatar).then(res => res.arrayBuffer());
277
+ return Buffer.from(arrayBuffer);
278
+ },
279
+ toBase64: async () => {
280
+ const arrayBuffer = await fetch(event?.author?.avatar).then(res => res.arrayBuffer());
281
+ return Buffer.from(arrayBuffer).toString('base64');
282
+ },
283
+ toURL: async () => {
284
+ return event?.author?.avatar;
285
+ }
286
+ };
287
+ const UserKey = useUserHashKey({
288
+ Platform: platform,
289
+ UserId: UserId
290
+ });
291
+ // 定义消
292
+ const e = {
293
+ // 事件类型
294
+ Platform: platform,
295
+ //
296
+ GuildId: event.guild_id,
297
+ ChannelId: event.channel_id,
298
+ UserId: event?.author?.id ?? '',
299
+ UserKey,
300
+ UserName: event?.author?.username ?? '',
301
+ UserAvatar: UserAvatar,
302
+ IsMaster: isMaster,
303
+ IsBot: false,
304
+ // message
305
+ MessageId: event.id,
306
+ MessageText: msg,
307
+ OpenId: event.guild_id,
308
+ CreateAt: Date.now(),
309
+ //
310
+ tag: 'AT_MESSAGE_CREATE',
311
+ value: null
312
+ };
313
+ // 当访问的时候获取
314
+ Object.defineProperty(e, 'value', {
315
+ get() {
316
+ return event;
317
+ }
318
+ });
319
+ // 处理消息
320
+ OnProcessor(e, 'message.create');
321
+ });
322
+ // FRIEND_ADD
323
+ global.client = client;
324
+ return {
325
+ api: {
326
+ use: {
327
+ send: (event, val) => {
328
+ if (val.length < 0)
329
+ return Promise.all([]);
330
+ const content = val
331
+ .filter(item => item.type == 'Link' || item.type == 'Mention' || item.type == 'Text')
332
+ .map(item => {
333
+ if (item.type == 'Link') {
334
+ return `[${item.options?.title ?? item.value}](${item.value})`;
335
+ }
336
+ else if (item.type == 'Mention') {
337
+ if (item.value == 'everyone' ||
338
+ item.value == 'all' ||
339
+ item.value == '' ||
340
+ typeof item.value != 'string') {
341
+ return ``;
342
+ }
343
+ if (item.options?.belong == 'user') {
344
+ return `<@${item.value}>`;
345
+ }
346
+ return '';
347
+ // return `<qqbot-at-everyone />`
348
+ }
349
+ else if (item.type == 'Text') {
350
+ return item.value;
351
+ }
352
+ })
353
+ .join('');
354
+ if (content) {
355
+ return Promise.all([content].map(item => client.groupOpenMessages(event.GuildId, {
356
+ content: item,
357
+ msg_id: event.MessageId,
358
+ msg_type: 0,
359
+ msg_seq: client.getMessageSeq(event.MessageId)
360
+ })));
361
+ }
362
+ const images = val.filter(item => item.type == 'Image').map(item => item.value);
363
+ if (images) {
364
+ return Promise.all(images.map(async (msg) => {
365
+ const file_info = await client
366
+ .postRichMediaByGroup(event.GuildId, {
367
+ file_type: 1,
368
+ file_data: msg.toString('base64')
369
+ })
370
+ .then(res => res?.file_info);
371
+ if (!file_info)
372
+ return Promise.resolve(null);
373
+ return client.groupOpenMessages(event.GuildId, {
374
+ content: '',
375
+ media: {
376
+ file_info
377
+ },
378
+ msg_id: event.MessageId,
379
+ msg_type: 7,
380
+ msg_seq: client.getMessageSeq(event.MessageId)
381
+ });
382
+ }));
383
+ }
384
+ return Promise.all([]);
385
+ },
386
+ mention: async () => {
387
+ // const event = e.value
388
+ const Metions = [];
389
+ return Metions;
390
+ }
391
+ }
392
+ }
393
+ };
394
+ });
395
+
396
+ export { index as default, platform };
package/lib/webhook.js ADDED
@@ -0,0 +1,53 @@
1
+ import { ed25519 } from '@noble/curves/ed25519';
2
+
3
+ class WebhookAPI {
4
+ config;
5
+ constructor(config) {
6
+ this.config = config;
7
+ }
8
+ /**
9
+ * 验证签名
10
+ * @param ts
11
+ * @param body
12
+ * @param sign
13
+ * @returns
14
+ */
15
+ validSign(ts, body, sign) {
16
+ const { publicKey } = this.getKey();
17
+ const sig = Buffer.isBuffer(sign) ? sign : Buffer.from(sign, 'hex');
18
+ const httpBody = Buffer.from(body);
19
+ const msg = Buffer.from(ts + httpBody);
20
+ return ed25519.verify(sig, msg, publicKey);
21
+ }
22
+ /**
23
+ * 生成签名
24
+ * @param eventTs
25
+ * @param plainToken
26
+ * @returns
27
+ */
28
+ getSign(eventTs, plainToken) {
29
+ const { privateKey } = this.getKey();
30
+ const msg = Buffer.from(eventTs + plainToken);
31
+ const signature = Buffer.from(ed25519.sign(msg, privateKey)).toString('hex');
32
+ return signature;
33
+ }
34
+ /**
35
+ * 获取 key
36
+ * @returns
37
+ */
38
+ getKey() {
39
+ let seed = this.config.secret;
40
+ if (!seed)
41
+ throw new Error("secret not set, can't calc ed25519 key");
42
+ while (seed.length < 32)
43
+ seed = seed.repeat(2); // Ed25519 的种子大小是 32 字节
44
+ seed = seed.slice(0, 32); // 修剪到 32 字节
45
+ const privateKey = Buffer.from(seed);
46
+ return {
47
+ privateKey,
48
+ publicKey: ed25519.getPublicKey(privateKey)
49
+ };
50
+ }
51
+ }
52
+
53
+ export { WebhookAPI };