├── .gitignore
├── README.md
├── index.js
└── package.json
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # TRSS-Yunzai QQBot Plugin
4 |
5 | TRSS-Yunzai QQBot 适配器 插件
6 |
7 | [](https://github.com/TimeRainStarSky/Yunzai-QQBot-Plugin)
8 | [](../../stargazers)
9 | [](../../archive/main.tar.gz)
10 | [](../../releases/latest)
11 |
12 | [](https://github.com/TimeRainStarSky/Yunzai-QQBot-Plugin)
13 |
14 |
15 |
16 | ## 安装教程
17 |
18 | 1. 准备:[TRSS-Yunzai](../../../Yunzai)
19 | 2. 输入:`#安装QQBot-Plugin`
20 | 3. 打开:[QQ 开放平台](https://q.qq.com) 创建 Bot:
21 | ① 创建机器人
22 | ② 开发设置 → 得到 `机器人QQ号:AppID:Token:AppSecret`
23 | 4. 输入:`#QQBot设置机器人QQ号:AppID:Token:AppSecret:[012]:[01]`
24 |
25 | ## 格式示例
26 |
27 | - 机器人QQ号 `114` AppID `514` Token `1919` AppSecret `810` 群Bot 频道私域
28 |
29 | ```
30 | #QQBot设置114:514:1919:810:1:1
31 | ```
32 |
33 | - WebHook
34 |
35 | ```
36 | #QQBot设置114:514:1919:810:2
37 | ```
38 |
39 | 需要启用公网 HTTPS,开放平台添加 url/QQBot
40 |
41 | ## 使用教程
42 |
43 | - #QQBot账号
44 | - #QQBot设置 + `机器人QQ号:AppID:Token:AppSecret:是否群Bot:是否频道私域`(是1 否0)
45 | - #QQBotMD + `机器人QQ号:raw`(需要原生MD按钮权限)
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | logger.info(logger.yellow("- 正在加载 QQBot 适配器插件"))
2 |
3 | import makeConfig from "../../lib/plugins/config.js"
4 | import fs from "node:fs/promises"
5 | import path from "node:path"
6 | import QRCode from "qrcode"
7 | import { ulid } from "ulid"
8 | import imageSize from "image-size"
9 | import urlRegexSafe from "url-regex-safe"
10 | import { encode as encodeSilk, isSilk } from "silk-wasm"
11 | import { Bot as QQBot } from "qq-group-bot"
12 |
13 | const { config, configSave } = await makeConfig("QQBot", {
14 | tips: "",
15 | permission: "master",
16 | toQRCode: true,
17 | toCallback: true,
18 | toBotUpload: true,
19 | hideGuildRecall: false,
20 | imageLength: 3,
21 | markdown: {
22 | template: "abcdefghij",
23 | },
24 | bot: {
25 | sandbox: false,
26 | maxRetry: Infinity,
27 | timeout: 30000,
28 | },
29 | token: [],
30 | }, {
31 | tips: [
32 | "欢迎使用 TRSS-Yunzai QQBot Plugin ! 作者:时雨🌌星空",
33 | "参考:https://github.com/TimeRainStarSky/Yunzai-QQBot-Plugin",
34 | ],
35 | })
36 |
37 | let sharp
38 | if (config.imageLength) try {
39 | sharp = (await import("sharp")).default
40 | } catch (err) {
41 | Bot.makeLog("error", ["sharp 导入错误,图片压缩关闭", err], "QQBot-Plugin")
42 | }
43 |
44 | const adapter = new class QQBotAdapter {
45 | constructor() {
46 | this.id = "QQBot"
47 | this.name = "QQBot"
48 | this.path = "data/QQBot/"
49 | this.version = "qq-group-bot v1.1.0"
50 |
51 | switch (typeof config.toQRCode) {
52 | case "boolean":
53 | this.toQRCodeRegExp = config.toQRCode ? urlRegexSafe() : false
54 | break
55 | case "string":
56 | this.toQRCodeRegExp = new RegExp(config.toQRCode, "g")
57 | break
58 | case "object":
59 | this.toQRCodeRegExp = urlRegexSafe(config.toQRCode)
60 | break
61 | }
62 |
63 | this.sep = ":"
64 | if (process.platform === "win32")
65 | this.sep = ""
66 | this.bind_user = {}
67 | this.appid = {}
68 | }
69 |
70 | async makeRecord(file) {
71 | if (config.toBotUpload) for (const i of Bot.uin) {
72 | if (!Bot[i].uploadRecord) continue
73 | try {
74 | const url = await Bot[i].uploadRecord(file)
75 | if (url) return url
76 | } catch (err) {
77 | Bot.makeLog("error", ["Bot", i, "语音上传错误", file, err])
78 | }
79 | }
80 | const buffer = await Bot.Buffer(file)
81 | if (!Buffer.isBuffer(buffer)) return file
82 | if (isSilk(buffer)) return buffer
83 |
84 | const convFile = path.join("temp", ulid())
85 | try {
86 | await fs.writeFile(convFile, buffer)
87 | await Bot.exec(`ffmpeg -i "${convFile}" -f s16le -ar 48000 -ac 1 "${convFile}.pcm"`)
88 | file = Buffer.from((await encodeSilk(await fs.readFile(`${convFile}.pcm`), 48000)).data)
89 | } catch (err) {
90 | Bot.makeLog("error", ["silk 转码错误", file, err])
91 | }
92 |
93 | for (const i of [convFile, `${convFile}.pcm`])
94 | fs.unlink(i).catch(() => {})
95 |
96 | return file
97 | }
98 |
99 | async makeQRCode(data) {
100 | return (await QRCode.toDataURL(data)).replace("data:image/png;base64,", "base64://")
101 | }
102 |
103 | async makeRawMarkdownText(data, text, button) {
104 | const match = text.match(this.toQRCodeRegExp)
105 | if (match) for (const url of match) {
106 | button.push(...this.makeButtons(data, [[{ text: url, link: url }]]))
107 | const img = await this.makeMarkdownImage(data, await this.makeQRCode(url), "二维码")
108 | text = text.replace(url, `${img.des}${img.url}`)
109 | }
110 | return text.replace(/@/g, "@")
111 | }
112 |
113 | async makeBotImage(file) {
114 | if (config.toBotUpload) for (const i of Bot.uin) {
115 | if (!Bot[i].uploadImage) continue
116 | try {
117 | const image = await Bot[i].uploadImage(file)
118 | if (image.url) return image
119 | } catch (err) {
120 | Bot.makeLog("error", ["Bot", i, "图片上传错误", file, err])
121 | }
122 | }
123 | }
124 |
125 | async makeMarkdownImage(data, file, summary = "图片") {
126 | const buffer = await Bot.Buffer(file)
127 | const image = await this.makeBotImage(buffer) ||
128 | { url: await Bot.fileToUrl(file) }
129 |
130 | if (!image.width || !image.height) try {
131 | const size = imageSize(buffer)
132 | image.width = size.width
133 | image.height = size.height
134 | } catch (err) {
135 | Bot.makeLog("error", ["图片分辨率检测错误", file, err], data.self_id)
136 | }
137 |
138 | return {
139 | des: `![${summary} #${image.width || 0}px #${image.height || 0}px]`,
140 | url: `(${image.url})`,
141 | }
142 | }
143 |
144 | makeButton(data, button, style) {
145 | const msg = {
146 | id: ulid(),
147 | render_data: {
148 | label: button.text,
149 | visited_label: button.clicked_text,
150 | style,
151 | ...button.QQBot?.render_data,
152 | }
153 | }
154 |
155 | if (button.input)
156 | msg.action = {
157 | type: 2,
158 | permission: { type: 2 },
159 | data: button.input,
160 | enter: button.send,
161 | ...button.QQBot?.action,
162 | }
163 | else if (button.callback) {
164 | if (config.toCallback) {
165 | msg.action = {
166 | type: 1,
167 | permission: { type: 2 },
168 | ...button.QQBot?.action,
169 | }
170 | if (!Array.isArray(data._ret_id))
171 | data._ret_id = []
172 | data.bot.callback[msg.id] = {
173 | id: data.message_id,
174 | user_id: data.user_id,
175 | group_id: data.group_id,
176 | message: button.callback,
177 | message_id: data._ret_id,
178 | }
179 | setTimeout(() => delete data.bot.callback[msg.id], 300000)
180 | } else {
181 | msg.action = {
182 | type: 2,
183 | permission: { type: 2 },
184 | data: button.callback,
185 | enter: true,
186 | ...button.QQBot?.action,
187 | }
188 | }
189 | } else if (button.link)
190 | msg.action = {
191 | type: 0,
192 | permission: { type: 2 },
193 | data: button.link,
194 | ...button.QQBot?.action,
195 | }
196 | else return false
197 |
198 | if (button.permission) {
199 | if (button.permission === "admin") {
200 | msg.action.permission.type = 1
201 | } else {
202 | msg.action.permission.type = 0
203 | msg.action.permission.specify_user_ids = []
204 | if (!Array.isArray(button.permission))
205 | button.permission = [button.permission]
206 | for (const id of button.permission)
207 | msg.action.permission.specify_user_ids.push(id.replace(`${data.self_id}${this.sep}`, ""))
208 | }
209 | }
210 | return msg
211 | }
212 |
213 | makeButtons(data, button_square) {
214 | const msgs = [], random = Math.floor(Math.random()*2)
215 | for (const button_row of button_square) {
216 | let column = 0
217 | const buttons = []
218 | for (let button of button_row) {
219 | button = this.makeButton(data, button,
220 | (random+msgs.length+buttons.length)%2)
221 | if (button) buttons.push(button)
222 | }
223 | if (buttons.length)
224 | msgs.push({ type: "button", buttons })
225 | }
226 | return msgs
227 | }
228 |
229 | async makeRawMarkdownMsg(data, msg) {
230 | const messages = [], button = []
231 | let content = "", reply
232 |
233 | for (let i of Array.isArray(msg) ? msg : [msg]) {
234 | if (typeof i === "object")
235 | i = { ...i }
236 | else
237 | i = { type: "text", text: Bot.String(i) }
238 |
239 | switch (i.type) {
240 | case "record":
241 | i.type = "audio"
242 | i.file = await this.makeRecord(i.file)
243 | case "video":
244 | case "face":
245 | case "ark":
246 | case "embed":
247 | messages.push([i])
248 | break
249 | case "file":
250 | if (i.file) i.file = await Bot.fileToUrl(i.file, i)
251 | content += await this.makeRawMarkdownText(data, `文件:${i.file}`, button)
252 | break
253 | case "at":
254 | if (i.qq === "all")
255 | content += "@everyone"
256 | else
257 | content += `<@${i.qq?.replace?.(`${data.self_id}${this.sep}`, "")}>`
258 | break
259 | case "text":
260 | content += await this.makeRawMarkdownText(data, i.text, button)
261 | break
262 | case "image": {
263 | const { des, url } = await this.makeMarkdownImage(data, i.file, i.summary)
264 | content += `${des}${url}`
265 | break
266 | } case "markdown":
267 | if (typeof i.data === "object")
268 | messages.push([{ type: "markdown", ...i.data }])
269 | else
270 | content += i.data
271 | break
272 | case "button":
273 | button.push(...this.makeButtons(data, i.data))
274 | break
275 | case "reply":
276 | if (i.id.startsWith("event_"))
277 | reply = { type: "reply", event_id: i.id.replace(/^event_/, "") }
278 | else
279 | reply = i
280 | continue
281 | case "node":
282 | for (const { message } of i.data)
283 | messages.push(...(await this.makeRawMarkdownMsg(data, message)))
284 | continue
285 | case "raw":
286 | messages.push(Array.isArray(i.data) ? i.data : [i.data])
287 | break
288 | default:
289 | content += await this.makeRawMarkdownText(data, Bot.String(i), button)
290 | }
291 | }
292 |
293 | if (content)
294 | messages.unshift([{ type: "markdown", content }])
295 |
296 | if (button.length) {
297 | for (const i of messages) {
298 | if (i[0].type === "markdown")
299 | i.push(...button.splice(0,5))
300 | if (!button.length) break
301 | }
302 | while (button.length)
303 | messages.push([
304 | { type: "markdown", content: " " },
305 | ...button.splice(0,5),
306 | ])
307 | }
308 |
309 | if (reply) for (const i in messages) {
310 | if (Array.isArray(messages[i]))
311 | messages[i].unshift(reply)
312 | else
313 | messages[i] = [reply, messages[i]]
314 | }
315 | return messages
316 | }
317 |
318 | makeMarkdownText_(data, text, button) {
319 | const match = text.match(this.toQRCodeRegExp)
320 | if (match) for (const url of match) {
321 | button.push(...this.makeButtons(data, [[{ text: url, link: url }]]))
322 | text = text.replace(url, "[链接(请点击按钮查看)]")
323 | }
324 | return text.replace(/\n/g, "\r").replace(/@/g, "@")
325 | }
326 |
327 | makeMarkdownText(data, text, content, button) {
328 | const match = text.match(/!?\[.*?\]\s*\(\w+:\/\/.*?\)/g)
329 | if (match) {
330 | const temp = []
331 | let last = ""
332 | for (const i of match) {
333 | const match = i.match(/(!?\[.*?\])\s*(\(\w+:\/\/.*?\))/)
334 | text = text.split(i)
335 | temp.push([last+this.makeMarkdownText_(data, text.shift(), button), match[1]])
336 | text = text.join(i)
337 | last = match[2]
338 | }
339 | temp[0][0] = content + temp[0][0]
340 | return [last+this.makeMarkdownText_(data, text, button), temp]
341 | }
342 | return [this.makeMarkdownText_(data, text, button)]
343 | }
344 |
345 | makeMarkdownTemplate(data, templates) {
346 | const msgs = []
347 | for (const template of templates) {
348 | if (!template.length) continue
349 |
350 | const params = []
351 | for (const i in template)
352 | params.push({
353 | key: config.markdown.template[i],
354 | values: [template[i]],
355 | })
356 |
357 | msgs.push([{
358 | type: "markdown",
359 | custom_template_id: config.markdown[data.self_id],
360 | params,
361 | }])
362 | }
363 | return msgs
364 | }
365 |
366 | makeMarkdownTemplatePush(content, template, templates) {
367 | for (const i of content) {
368 | if (template.length === config.markdown.template.length-1) {
369 | template.push(i.shift())
370 | template = i
371 | templates.push(template)
372 | } else {
373 | template.push(i.join(""))
374 | }
375 | }
376 | return template
377 | }
378 |
379 | async makeMarkdownMsg(data, msg) {
380 | const messages = [], button = [], templates = [[]]
381 | let content = "", reply, template = templates[0]
382 |
383 | for (let i of Array.isArray(msg) ? msg : [msg]) {
384 | if (typeof i === "object")
385 | i = { ...i }
386 | else
387 | i = { type: "text", text: Bot.String(i) }
388 |
389 | switch (i.type) {
390 | case "record":
391 | i.type = "audio"
392 | i.file = await this.makeRecord(i.file)
393 | case "video":
394 | case "face":
395 | case "ark":
396 | case "embed":
397 | messages.push([i])
398 | break
399 | case "file":
400 | if (i.file) i.file = await Bot.fileToUrl(i.file, i)
401 | button.push(...this.makeButtons(data, [[{ text: i.name || i.file, link: i.file }]]))
402 | content += "[文件(请点击按钮查看)]"
403 | break
404 | case "at":
405 | if (i.qq === "all")
406 | content += "@everyone"
407 | else
408 | content += `<@${i.qq?.replace?.(`${data.self_id}${this.sep}`, "")}>`
409 | break
410 | case "text": {
411 | const [text, temp] = this.makeMarkdownText(data, i.text, content, button)
412 | if (Array.isArray(temp)) {
413 | template = this.makeMarkdownTemplatePush(temp, template, templates)
414 | content = text
415 | } else {
416 | content += text
417 | }
418 | break
419 | } case "image": {
420 | const { des, url } = await this.makeMarkdownImage(data, i.file, i.summary)
421 | template = this.makeMarkdownTemplatePush([[content, des]], template, templates)
422 | content = url
423 | break
424 | } case "markdown":
425 | if (typeof i.data === "object")
426 | messages.push([{ type: "markdown", ...i.data }])
427 | else
428 | content += i.data
429 | break
430 | case "button":
431 | button.push(...this.makeButtons(data, i.data))
432 | break
433 | case "reply":
434 | if (i.id.startsWith("event_"))
435 | reply = { type: "reply", event_id: i.id.replace(/^event_/, "") }
436 | else
437 | reply = i
438 | continue
439 | case "node":
440 | for (const { message } of i.data)
441 | messages.push(...(await this.makeMarkdownMsg(data, message)))
442 | continue
443 | case "raw":
444 | messages.push(Array.isArray(i.data) ? i.data : [i.data])
445 | break
446 | default: {
447 | const [text, temp] = this.makeMarkdownText(data, Bot.String(i), content, button)
448 | if (Array.isArray(temp)) {
449 | template = this.makeMarkdownTemplatePush(temp, template, templates)
450 | content = text
451 | } else {
452 | content += text
453 | }
454 | }
455 | }
456 | }
457 |
458 | if (content)
459 | template.push(content)
460 | messages.push(...this.makeMarkdownTemplate(data, templates))
461 |
462 | if (button.length) {
463 | for (const i of messages) {
464 | if (i[0].type === "markdown")
465 | i.push(...button.splice(0,5))
466 | if (!button.length) break
467 | }
468 | while (button.length)
469 | messages.push([
470 | ...this.makeMarkdownTemplate(data, [[" "]])[0],
471 | ...button.splice(0,5),
472 | ])
473 | }
474 |
475 | if (reply) for (const i of messages)
476 | i.unshift(reply)
477 | return messages
478 | }
479 |
480 | async compressImage(data, file) {
481 | try {
482 | const size = config.imageLength * 1024 * 1024
483 | const buffer = await Bot.Buffer(file, { http: true })
484 |
485 | if (!Buffer.isBuffer(buffer))
486 | return file
487 |
488 | if (buffer.length <= size)
489 | return buffer
490 |
491 | let quality = 105, output
492 | do {
493 | quality -= 10
494 | output = await sharp(buffer).jpeg({ quality }).toBuffer()
495 | Bot.makeLog("debug", `图片压缩完成 ${quality}%(${(output.length/1024).toFixed(2)}KB)`, data.self_id)
496 | } while (output.length > size && quality > 10)
497 |
498 | return output
499 | } catch (err) {
500 | Bot.makeLog("error", ["图片压缩错误", err], data.self_id)
501 | return file
502 | }
503 | }
504 |
505 | async makeMsg(data, msg) {
506 | const messages = [], button = []
507 | let message = [], reply
508 |
509 | for (let i of Array.isArray(msg) ? msg : [msg]) {
510 | if (typeof i === "object")
511 | i = { ...i }
512 | else
513 | i = { type: "text", text: Bot.String(i) }
514 |
515 | switch (i.type) {
516 | case "at":
517 | //i.user_id = i.qq?.replace?.(`${data.self_id}${this.sep}`, "")
518 | continue
519 | case "text":
520 | if (!i.text || !i.text.trim()) continue
521 | break
522 | case "face":
523 | case "ark":
524 | case "embed":
525 | break
526 | case "record":
527 | i.type = "audio"
528 | i.file = await this.makeRecord(i.file)
529 | case "video":
530 | case "image":
531 | if (message.length) {
532 | messages.push(message)
533 | message = []
534 | }
535 |
536 | if (sharp && i.file)
537 | i.file = await this.compressImage(data, i.file)
538 | break
539 | case "file":
540 | if (i.file) i.file = await Bot.fileToUrl(i.file, i)
541 | i = { type: "text", text: `文件:${i.file}` }
542 | break
543 | case "reply":
544 | if (i.id.startsWith("event_"))
545 | reply = { type: "reply", event_id: i.id.replace(/^event_/, "") }
546 | else
547 | reply = i
548 | continue
549 | case "markdown":
550 | if (typeof i.data === "object")
551 | i = { type: "markdown", ...i.data }
552 | else
553 | i = { type: "markdown", content: i.data }
554 | break
555 | case "button":
556 | //button.push(...this.makeButtons(data, i.data))
557 | continue
558 | case "node":
559 | for (const { message } of i.data)
560 | messages.push(...(await this.makeMsg(data, message)))
561 | continue
562 | case "raw":
563 | if (Array.isArray(i.data)) {
564 | messages.push(i.data)
565 | continue
566 | }
567 | i = i.data
568 | break
569 | default:
570 | i = { type: "text", text: Bot.String(i) }
571 | }
572 |
573 | if (i.type === "text" && i.text) {
574 | const match = i.text.match(this.toQRCodeRegExp)
575 | if (match) for (const url of match) {
576 | const msg = segment.image(await this.makeQRCode(url))
577 | if (message.length) {
578 | messages.push(message)
579 | message = []
580 | }
581 | message.push(msg)
582 | i.text = i.text.replace(url, "[链接(请扫码查看)]")
583 | }
584 | }
585 |
586 | message.push(i)
587 | }
588 |
589 | if (message.length)
590 | messages.push(message)
591 |
592 | while (button.length)
593 | messages.push([{
594 | type: "keyboard",
595 | content: { rows: button.splice(0,5) },
596 | }])
597 |
598 | if (reply) for (const i of messages)
599 | i.unshift(reply)
600 | return messages
601 | }
602 |
603 | async sendMsg(data, send, msg) {
604 | const rets = { message_id: [], data: [], error: [] }
605 | let msgs
606 |
607 | const sendMsg = async () => { for (const i of msgs) try {
608 | Bot.makeLog("debug", ["发送消息", i], data.self_id)
609 | const ret = await send(i)
610 | Bot.makeLog("debug", ["发送消息返回", ret], data.self_id)
611 |
612 | rets.data.push(ret)
613 | if (ret.id)
614 | rets.message_id.push(ret.id)
615 | } catch (err) {
616 | Bot.makeLog("error", ["发送消息错误", i, err], data.self_id)
617 | rets.error.push(err)
618 | return false
619 | }}
620 |
621 | if (config.markdown[data.self_id]) {
622 | if (config.markdown[data.self_id] === "raw")
623 | msgs = await this.makeRawMarkdownMsg(data, msg)
624 | else
625 | msgs = await this.makeMarkdownMsg(data, msg)
626 | } else {
627 | msgs = await this.makeMsg(data, msg)
628 | }
629 |
630 | if (await sendMsg() === false) {
631 | msgs = await this.makeMsg(data, msg)
632 | await sendMsg()
633 | }
634 |
635 | if (Array.isArray(data._ret_id))
636 | data._ret_id.push(...rets.message_id)
637 | return rets
638 | }
639 |
640 | sendFriendMsg(data, msg, event) {
641 | return this.sendMsg(data, msg => data.bot.sdk.sendPrivateMessage(data.user_id, msg, event), msg)
642 | }
643 |
644 | sendGroupMsg(data, msg, event) {
645 | return this.sendMsg(data, msg => data.bot.sdk.sendGroupMessage(data.group_id, msg, event), msg)
646 | }
647 |
648 | async makeGuildMsg(data, msg) {
649 | const messages = []
650 | let message = [], reply
651 | for (let i of Array.isArray(msg) ? msg : [msg]) {
652 | if (typeof i === "object")
653 | i = { ...i }
654 | else
655 | i = { type: "text", text: Bot.String(i) }
656 |
657 | switch (i.type) {
658 | case "at":
659 | i.user_id = i.qq?.replace?.(/^qg_/, "")
660 | case "text":
661 | case "face":
662 | case "ark":
663 | case "embed":
664 | break
665 | case "image":
666 | message.push(i)
667 | messages.push(message)
668 | message = []
669 | continue
670 | case "record":
671 | case "video":
672 | case "file":
673 | if (i.file) i.file = await Bot.fileToUrl(i.file, i)
674 | i = { type: "text", text: `文件:${i.file}` }
675 | break
676 | case "reply":
677 | reply = i
678 | continue
679 | case "markdown":
680 | if (typeof i.data === "object")
681 | i = { type: "markdown", ...i.data }
682 | else
683 | i = { type: "markdown", content: i.data }
684 | break
685 | case "button":
686 | continue
687 | case "node":
688 | for (const { message } of i.data)
689 | messages.push(...(await this.makeGuildMsg(data, message)))
690 | continue
691 | case "raw":
692 | if (Array.isArray(i.data)) {
693 | messages.push(i.data)
694 | continue
695 | }
696 | i = i.data
697 | break
698 | default:
699 | i = { type: "text", text: Bot.String(i) }
700 | }
701 |
702 | if (i.type === "text" && i.text) {
703 | const match = i.text.match(this.toQRCodeRegExp)
704 | if (match) for (const url of match) {
705 | const msg = segment.image(await this.makeQRCode(url))
706 | message.push(msg)
707 | messages.push(message)
708 | message = []
709 | i.text = i.text.replace(url, "[链接(请扫码查看)]")
710 | }
711 | }
712 |
713 | message.push(i)
714 | }
715 |
716 | if (message.length)
717 | messages.push(message)
718 | if (reply) for (const i of messages)
719 | i.unshift(reply)
720 | return messages
721 | }
722 |
723 | async sendGMsg(data, send, msg) {
724 | const rets = { message_id: [], data: [], error: [] }
725 | let msgs
726 |
727 | const sendMsg = async () => { for (const i of msgs) try {
728 | Bot.makeLog("debug", ["发送消息", i], data.self_id)
729 | const ret = await send(i)
730 | Bot.makeLog("debug", ["发送消息返回", ret], data.self_id)
731 |
732 | rets.data.push(ret)
733 | if (ret.id)
734 | rets.message_id.push(ret.id)
735 | } catch (err) {
736 | Bot.makeLog("error", ["发送消息错误", i, err], data.self_id)
737 | rets.error.push(err)
738 | return false
739 | }}
740 |
741 | msgs = await this.makeGuildMsg(data, msg)
742 | if (await sendMsg() === false) {
743 | msgs = await this.makeGuildMsg(data, msg)
744 | await sendMsg()
745 | }
746 | return rets
747 | }
748 |
749 | async sendDirectMsg(data, msg, event) {
750 | if (!data.guild_id) {
751 | if (!data.src_guild_id) {
752 | Bot.makeLog("error", [`发送频道私聊消息失败:[${data.user_id}] 不存在来源频道信息`, msg], data.self_id)
753 | return false
754 | }
755 | const dms = await data.bot.sdk.createDirectSession(data.src_guild_id, data.user_id)
756 | data.guild_id = dms.guild_id
757 | data.channel_id = dms.channel_id
758 | data.bot.fl.set(`qg_${data.user_id}`, {
759 | ...data.bot.fl.get(`qg_${data.user_id}`),
760 | ...dms,
761 | })
762 | }
763 | return this.sendGMsg(data, msg => data.bot.sdk.sendDirectMessage(data.guild_id, msg, event), msg)
764 | }
765 |
766 | sendGuildMsg(data, msg, event) {
767 | return this.sendGMsg(data, msg => data.bot.sdk.sendGuildMessage(data.channel_id, msg, event), msg)
768 | }
769 |
770 | async recallMsg(data, recall, message_id) {
771 | if (!Array.isArray(message_id))
772 | message_id = [message_id]
773 | const msgs = []
774 | for (const i of message_id) try {
775 | msgs.push(await recall(i))
776 | } catch (err) {
777 | Bot.makeLog("debug", ["撤回消息错误", i, err], data.self_id)
778 | msgs.push(false)
779 | }
780 | return msgs
781 | }
782 |
783 | recallFriendMsg(data, message_id) {
784 | Bot.makeLog("info", `撤回好友消息:[${data.user_id}] ${message_id}`, data.self_id)
785 | return this.recallMsg(data, i => data.bot.sdk.recallFriendMessage(data.user_id, i), message_id)
786 | }
787 |
788 | recallGroupMsg(data, message_id) {
789 | Bot.makeLog("info", `撤回群消息:[${data.group_id}] ${message_id}`, data.self_id)
790 | return this.recallMsg(data, i => data.bot.sdk.recallGroupMessage(data.group_id, i), message_id)
791 | }
792 |
793 | recallDirectMsg(data, message_id, hide = config.hideGuildRecall) {
794 | Bot.makeLog("info", `撤回${hide?"并隐藏":""}频道私聊消息:[${data.guild_id}] ${message_id}`, data.self_id)
795 | return this.recallMsg(data, i => data.bot.sdk.recallDirectMessage(data.guild_id, i, hide), message_id)
796 | }
797 |
798 | recallGuildMsg(data, message_id, hide = config.hideGuildRecall) {
799 | Bot.makeLog("info", `撤回${hide?"并隐藏":""}频道消息:[${data.channel_id}] ${message_id}`, data.self_id)
800 | return this.recallMsg(data, i => data.bot.sdk.recallGuildMessage(data.channel_id, i, hide), message_id)
801 | }
802 |
803 | pickFriend(id, user_id) {
804 | if (typeof user_id !== "string")
805 | user_id = String(user_id)
806 | else if (user_id.startsWith("qg_"))
807 | return this.pickGuildFriend(id, user_id)
808 | const i = {
809 | ...Bot[id].fl.get(user_id),
810 | self_id: id,
811 | bot: Bot[id],
812 | user_id: user_id.replace(`${id}${this.sep}`, ""),
813 | }
814 | return {
815 | ...i,
816 | sendMsg: msg => this.sendFriendMsg(i, msg),
817 | recallMsg: message_id => this.recallFriendMsg(i, message_id),
818 | getAvatarUrl: () => `https://q.qlogo.cn/qqapp/${i.bot.info.appid}/${i.user_id}/0`,
819 | }
820 | }
821 |
822 | pickMember(id, group_id, user_id) {
823 | if (typeof group_id !== "string")
824 | group_id = String(group_id)
825 | if (typeof user_id !== "string")
826 | user_id = String(user_id)
827 | else if (user_id.startsWith("qg_"))
828 | return this.pickGuildMember(id, group_id, user_id)
829 | const i = {
830 | ...Bot[id].fl.get(user_id),
831 | ...Bot[id].gml.get(group_id)?.get(user_id),
832 | self_id: id,
833 | bot: Bot[id],
834 | user_id: user_id.replace(`${id}${this.sep}`, ""),
835 | group_id: group_id.replace(`${id}${this.sep}`, ""),
836 | }
837 | return {
838 | ...this.pickFriend(id, user_id),
839 | ...i,
840 | }
841 | }
842 |
843 | pickGroup(id, group_id) {
844 | if (typeof group_id !== "string")
845 | group_id = String(group_id)
846 | else if (group_id.startsWith("qg_"))
847 | return this.pickGuild(id, group_id)
848 | const i = {
849 | ...Bot[id].gl.get(group_id),
850 | self_id: id,
851 | bot: Bot[id],
852 | group_id: group_id.replace(`${id}${this.sep}`, ""),
853 | }
854 | return {
855 | ...i,
856 | sendMsg: msg => this.sendGroupMsg(i, msg),
857 | recallMsg: message_id => this.recallGroupMsg(i, message_id),
858 | pickMember: user_id => this.pickMember(id, group_id, user_id),
859 | getMemberMap: () => i.bot.gml.get(group_id),
860 | }
861 | }
862 |
863 | pickGuildFriend(id, user_id) {
864 | const i = {
865 | ...Bot[id].fl.get(user_id),
866 | self_id: id,
867 | bot: Bot[id],
868 | user_id: user_id.replace(/^qg_/, ""),
869 | }
870 | return {
871 | ...i,
872 | sendMsg: msg => this.sendDirectMsg(i, msg),
873 | recallMsg: (message_id, hide) => this.recallDirectMsg(i, message_id, hide),
874 | }
875 | }
876 |
877 | pickGuildMember(id, group_id, user_id) {
878 | const guild_id = group_id.replace(/^qg_/, "").split("-")
879 | const i = {
880 | ...Bot[id].fl.get(user_id),
881 | ...Bot[id].gml.get(group_id)?.get(user_id),
882 | self_id: id,
883 | bot: Bot[id],
884 | src_guild_id: guild_id[0],
885 | src_channel_id: guild_id[1],
886 | user_id: user_id.replace(/^qg_/, ""),
887 | }
888 | return {
889 | ...this.pickGuildFriend(id, user_id),
890 | ...i,
891 | sendMsg: msg => this.sendDirectMsg(i, msg),
892 | recallMsg: (message_id, hide) => this.recallDirectMsg(i, message_id, hide),
893 | }
894 | }
895 |
896 | pickGuild(id, group_id) {
897 | const guild_id = group_id.replace(/^qg_/, "").split("-")
898 | const i = {
899 | ...Bot[id].gl.get(group_id),
900 | self_id: id,
901 | bot: Bot[id],
902 | guild_id: guild_id[0],
903 | channel_id: guild_id[1],
904 | }
905 | return {
906 | ...i,
907 | sendMsg: msg => this.sendGuildMsg(i, msg),
908 | recallMsg: (message_id, hide) => this.recallGuildMsg(i, message_id, hide),
909 | pickMember: user_id => this.pickGuildMember(id, group_id, user_id),
910 | getMemberMap: () => i.bot.gml.get(group_id),
911 | }
912 | }
913 |
914 | async makeFriendMessage(data, event) {
915 | data.sender = {
916 | user_id: `${data.self_id}${this.sep}${event.sender.user_id}`,
917 | }
918 | Bot.makeLog("info", `好友消息:[${data.user_id}] ${data.raw_message}`, data.self_id)
919 |
920 | data.reply = msg => this.sendFriendMsg({
921 | ...data, user_id: event.sender.user_id,
922 | }, msg, { id: data.message_id })
923 | await this.setFriendMap(data)
924 | }
925 |
926 | async makeGroupMessage(data, event) {
927 | data.sender = {
928 | user_id: `${data.self_id}${this.sep}${event.sender.user_id}`,
929 | }
930 | data.group_id = `${data.self_id}${this.sep}${event.group_id}`
931 | Bot.makeLog("info", `群消息:[${data.group_id}, ${data.user_id}] ${data.raw_message}`, data.self_id)
932 |
933 | data.reply = msg => this.sendGroupMsg({
934 | ...data, group_id: event.group_id,
935 | }, msg, { id: data.message_id })
936 | data.message.unshift({ type: "at", qq: data.self_id })
937 | await this.setGroupMap(data)
938 | }
939 |
940 | async makeDirectMessage(data, event) {
941 | data.sender = {
942 | ...data.bot.fl.get(`qg_${event.sender.user_id}`),
943 | ...event.sender,
944 | user_id: `qg_${event.sender.user_id}`,
945 | nickname: event.sender.user_name,
946 | avatar: event.author.avatar,
947 | guild_id: event.guild_id,
948 | channel_id: event.channel_id,
949 | src_guild_id: event.src_guild_id,
950 | }
951 | Bot.makeLog("info", `频道私聊消息:[${data.sender.nickname}(${data.user_id})] ${data.raw_message}`, data.self_id)
952 |
953 | data.reply = msg => this.sendDirectMsg({
954 | ...data,
955 | user_id: event.user_id,
956 | guild_id: event.guild_id,
957 | channel_id: event.channel_id,
958 | }, msg, { id: data.message_id })
959 | await this.setFriendMap(data)
960 | }
961 |
962 | async makeGuildMessage(data, event) {
963 | data.message_type = "group"
964 | data.sender = {
965 | ...data.bot.fl.get(`qg_${event.sender.user_id}`),
966 | ...event.sender,
967 | user_id: `qg_${event.sender.user_id}`,
968 | nickname: event.sender.user_name,
969 | card: event.member.nick,
970 | avatar: event.author.avatar,
971 | src_guild_id: event.guild_id,
972 | src_channel_id: event.channel_id,
973 | }
974 | data.group_id = `qg_${event.guild_id}-${event.channel_id}`
975 | Bot.makeLog("info", `频道消息:[${data.group_id}, ${data.sender.nickname}(${data.user_id})] ${data.raw_message}`, data.self_id)
976 | data.reply = msg => this.sendGuildMsg({
977 | ...data,
978 | guild_id: event.guild_id,
979 | channel_id: event.channel_id,
980 | }, msg, { id: data.message_id })
981 | await this.setFriendMap(data)
982 | await this.setGroupMap(data)
983 | }
984 |
985 | async setFriendMap(data) {
986 | if (!data.user_id) return
987 | await data.bot.fl.set(data.user_id, {
988 | ...data.bot.fl.get(data.user_id),
989 | ...data.sender,
990 | })
991 | }
992 |
993 | async setGroupMap(data) {
994 | if (!data.group_id) return
995 | await data.bot.gl.set(data.group_id, {
996 | ...data.bot.gl.get(data.group_id),
997 | group_id: data.group_id,
998 | })
999 | let gml = data.bot.gml.get(data.group_id)
1000 | if (!gml) {
1001 | gml = new Map
1002 | await data.bot.gml.set(data.group_id, gml)
1003 | }
1004 | await gml.set(data.user_id, {
1005 | ...gml.get(data.user_id),
1006 | ...data.sender,
1007 | })
1008 | }
1009 |
1010 | async makeMessage(id, event) {
1011 | const data = {
1012 | raw: event,
1013 | bot: Bot[id],
1014 | self_id: id,
1015 | post_type: event.post_type,
1016 | message_type: event.message_type,
1017 | sub_type: event.sub_type,
1018 | message_id: event.message_id,
1019 | get user_id() { return this.sender.user_id },
1020 | message: event.message,
1021 | raw_message: event.raw_message,
1022 | }
1023 |
1024 | for (const i of data.message) switch (i.type) {
1025 | case "at":
1026 | if (data.message_type === "group")
1027 | i.qq = `${data.self_id}${this.sep}${i.user_id}`
1028 | else
1029 | i.qq = `qg_${i.user_id}`
1030 | break
1031 | }
1032 |
1033 | switch (data.message_type) {
1034 | case "private":
1035 | if (data.sub_type === "friend")
1036 | await this.makeFriendMessage(data, event)
1037 | else
1038 | await this.makeDirectMessage(data, event)
1039 | break
1040 | case "group":
1041 | await this.makeGroupMessage(data, event)
1042 | break
1043 | case "guild":
1044 | await this.makeGuildMessage(data, event)
1045 | break
1046 | default:
1047 | Bot.makeLog("warn", ["未知消息", event], id)
1048 | return
1049 | }
1050 |
1051 | Bot.em(`${data.post_type}.${data.message_type}.${data.sub_type}`, data)
1052 | }
1053 |
1054 | async makeBotCallback(id, event, callback) {
1055 | const data = {
1056 | raw: event,
1057 | bot: Bot[callback.self_id],
1058 | self_id: callback.self_id,
1059 | post_type: "message",
1060 | message_id: event.event_id ? `event_${event.event_id}` : event.notice_id,
1061 | message_type: callback.group_id ? "group" : "private",
1062 | sub_type: "callback",
1063 | get user_id() { return this.sender.user_id },
1064 | sender: { user_id: `${id}${this.sep}${event.operator_id}` },
1065 | message: [],
1066 | raw_message: "",
1067 | }
1068 |
1069 | data.message.push(
1070 | { type: "at", qq: callback.self_id },
1071 | { type: "text", text: callback.message },
1072 | )
1073 | data.raw_message += callback.message
1074 |
1075 | if (callback.group_id) {
1076 | data.group_id = callback.group_id
1077 | data.group = data.bot.pickGroup(callback.group_id)
1078 | data.group_name = data.group.name
1079 | data.friend = Bot[id].pickFriend(data.user_id)
1080 | if (data.friend.real_id) {
1081 | data.friend = data.bot.pickFriend(data.friend.real_id)
1082 | data.member = data.group.pickMember(data.friend.user_id)
1083 | data.sender = {
1084 | ...await data.member.getInfo() || data.member,
1085 | }
1086 | } else {
1087 | if (Bot[id].callback[data.user_id])
1088 | return event.reply(3)
1089 | Bot[id].callback[data.user_id] = true
1090 |
1091 | let msg = `请先发送 #QQBot绑定用户${data.user_id}`
1092 | const real_id = callback.message.replace(/^#[Qq]+[Bb]ot绑定用户确认/, "").trim()
1093 | if (this.bind_user[real_id] === data.user_id) {
1094 | await Bot[id].fl.set(data.user_id, {
1095 | ...Bot[id].fl.get(data.user_id), real_id,
1096 | })
1097 | msg = `绑定成功 ${data.user_id} → ${real_id}`
1098 | }
1099 |
1100 | event.reply(0)
1101 | return data.group.sendMsg(msg)
1102 | }
1103 | Bot.makeLog("info", [`群按钮点击事件:[${data.group_name}(${data.group_id}), ${data.sender.nickname}(${data.user_id})]`, data.raw_message], data.self_id)
1104 | } else {
1105 | await Bot[id].fl.set(data.user_id, {
1106 | ...Bot[id].fl.get(data.user_id),
1107 | real_id: callback.user_id,
1108 | })
1109 | data.friend = data.bot.pickFriend(callback.user_id)
1110 | data.sender = {
1111 | ...await data.friend.getInfo() || data.friend,
1112 | }
1113 | Bot.makeLog("info", [`好友按钮点击事件:[${data.sender.nickname}(${data.user_id})]`, data.raw_message], data.self_id)
1114 | }
1115 |
1116 | event.reply(0)
1117 | Bot.em(`${data.post_type}.${data.message_type}.${data.sub_type}`, data)
1118 | }
1119 |
1120 | async makeCallback(id, event) {
1121 | const reply = event.reply.bind(event)
1122 | event.reply = async (...args) => { try {
1123 | return await reply(...args)
1124 | } catch (err) {
1125 | Bot.makeLog("debug", ["回复按钮点击事件错误", err], data.self_id)
1126 | }}
1127 |
1128 | const data = {
1129 | raw: event,
1130 | bot: Bot[id],
1131 | self_id: id,
1132 | post_type: "message",
1133 | message_id: event.event_id ? `event_${event.event_id}` : event.notice_id,
1134 | message_type: event.notice_type,
1135 | sub_type: "callback",
1136 | get user_id() { return this.sender.user_id },
1137 | sender: { user_id: `${id}${this.sep}${event.operator_id}` },
1138 | message: [],
1139 | raw_message: "",
1140 | }
1141 |
1142 | const callback = data.bot.callback[event.data?.resolved?.button_id]
1143 | if (callback) {
1144 | if (callback.self_id)
1145 | return this.makeBotCallback(id, event, callback)
1146 | if (!event.group_id && callback.group_id)
1147 | event.group_id = callback.group_id
1148 | data.message_id = callback.id
1149 | if (callback.message_id.length) {
1150 | for (const id of callback.message_id)
1151 | data.message.push({ type: "reply", id })
1152 | data.raw_message += `[回复:${callback.message_id}]`
1153 | }
1154 | data.message.push({ type: "text", text: callback.message })
1155 | data.raw_message += callback.message
1156 | } else {
1157 | if (event.data?.resolved?.button_id) {
1158 | data.message.push({ type: "reply", id: event.data?.resolved?.button_id })
1159 | data.raw_message += `[回复:${event.data?.resolved?.button_id}]`
1160 | }
1161 | if (event.data?.resolved?.button_data) {
1162 | data.message.push({ type: "text", text: event.data?.resolved?.button_data })
1163 | data.raw_message += event.data?.resolved?.button_data
1164 | } else {
1165 | event.reply(1)
1166 | }
1167 | }
1168 | event.reply(0)
1169 |
1170 | switch (data.message_type) {
1171 | case "friend":
1172 | data.message_type = "private"
1173 | Bot.makeLog("info", [`好友按钮点击事件:[${data.user_id}]`, data.raw_message], data.self_id)
1174 |
1175 | data.reply = msg => this.sendFriendMsg({ ...data, user_id: event.operator_id }, msg, { id: data.message_id })
1176 | await this.setFriendMap(data)
1177 | break
1178 | case "group":
1179 | data.group_id = `${id}${this.sep}${event.group_id}`
1180 | Bot.makeLog("info", [`群按钮点击事件:[${data.group_id}, ${data.user_id}]`, data.raw_message], data.self_id)
1181 |
1182 | data.reply = msg => this.sendGroupMsg({ ...data, group_id: event.group_id }, msg, { id: data.message_id })
1183 | await this.setGroupMap(data)
1184 | break
1185 | case "guild":
1186 | break
1187 | default:
1188 | Bot.makeLog("warn", ["未知按钮点击事件", event], data.self_id)
1189 | }
1190 |
1191 | Bot.em(`${data.post_type}.${data.message_type}.${data.sub_type}`, data)
1192 | }
1193 |
1194 | makeNotice(id, event) {
1195 | const data = {
1196 | raw: event,
1197 | bot: Bot[id],
1198 | self_id: id,
1199 | post_type: event.post_type,
1200 | notice_type: event.notice_type,
1201 | sub_type: event.sub_type,
1202 | notice_id: event.notice_id,
1203 | }
1204 |
1205 | switch (data.sub_type) {
1206 | case "action":
1207 | return this.makeCallback(id, event)
1208 | case "increase":
1209 | case "decrease":
1210 | case "update":
1211 | case "member.increase":
1212 | case "member.decrease":
1213 | case "member.update":
1214 | break
1215 | default:
1216 | Bot.makeLog("warn", ["未知通知", event], id)
1217 | return
1218 | }
1219 |
1220 | //Bot.em(`${data.post_type}.${data.notice_type}.${data.sub_type}`, data)
1221 | }
1222 |
1223 | getFriendMap(id) {
1224 | return Bot.getMap(`${this.path}${id}/Friend`)
1225 | }
1226 |
1227 | getGroupMap(id) {
1228 | return Bot.getMap(`${this.path}${id}/Group`)
1229 | }
1230 |
1231 | getMemberMap(id) {
1232 | return Bot.getMap(`${this.path}${id}/Member`)
1233 | }
1234 |
1235 | async connect(token) {
1236 | token = token.split(":")
1237 | const id = token[0]
1238 | const opts = {
1239 | ...config.bot,
1240 | appid: token[1],
1241 | token: token[2],
1242 | secret: token[3],
1243 | intents: [
1244 | "GUILDS",
1245 | "GUILD_MEMBERS",
1246 | "GUILD_MESSAGE_REACTIONS",
1247 | "DIRECT_MESSAGE",
1248 | "INTERACTION",
1249 | "MESSAGE_AUDIT",
1250 | ],
1251 | }
1252 |
1253 | if (Number(token[4]))
1254 | opts.intents.push("GROUP_AT_MESSAGE_CREATE", "C2C_MESSAGE_CREATE")
1255 |
1256 | if (Number(token[5]))
1257 | opts.intents.push("GUILD_MESSAGES")
1258 | else
1259 | opts.intents.push("PUBLIC_GUILD_MESSAGES")
1260 |
1261 | Bot[id] = {
1262 | adapter: this,
1263 | sdk: new QQBot(opts),
1264 | login() { return new Promise(resolve => {
1265 | this.sdk.sessionManager.once("READY", resolve)
1266 | this.sdk.start()
1267 | })},
1268 | logout() { return new Promise(resolve => {
1269 | this.sdk.ws.once("close", resolve)
1270 | this.sdk.stop()
1271 | })},
1272 |
1273 | uin: id,
1274 | info: {
1275 | id, ...opts,
1276 | avatar: `https://q.qlogo.cn/g?b=qq&s=0&nk=${this.uin}`,
1277 | },
1278 | get nickname() { return this.info.username },
1279 | get avatar() { return this.info.avatar },
1280 |
1281 | version: {
1282 | id: this.id,
1283 | name: this.name,
1284 | version: this.version,
1285 | },
1286 | stat: { start_time: Date.now()/1000 },
1287 |
1288 | pickFriend: user_id => this.pickFriend(id, user_id),
1289 | get pickUser() { return this.pickFriend },
1290 | getFriendMap() { return this.fl },
1291 | fl: await this.getFriendMap(id),
1292 |
1293 | pickMember: (group_id, user_id) => this.pickMember(id, group_id, user_id),
1294 | pickGroup: group_id => this.pickGroup(id, group_id),
1295 | getGroupMap() { return this.gl },
1296 | gl: await this.getGroupMap(id),
1297 | gml: await this.getMemberMap(id),
1298 |
1299 | callback: {},
1300 | }
1301 |
1302 | Bot[id].sdk.logger = {}
1303 | for (const i of ["trace", "debug", "info", "mark", "warn", "error", "fatal"])
1304 | Bot[id].sdk.logger[i] = (...args) => {
1305 | if (args[0]?.startsWith?.("recv from")) return
1306 | return Bot.makeLog(i, args, id)
1307 | }
1308 |
1309 | try {
1310 | if (token[4] === "2") {
1311 | await Bot[id].sdk.sessionManager.getAccessToken()
1312 | Bot[id].login = () => this.appid[opts.appid] = Bot[id]
1313 | Bot[id].logout = () => delete this.appid[opts.appid]
1314 | }
1315 |
1316 | await Bot[id].login()
1317 | Object.assign(Bot[id].info, await Bot[id].sdk.getSelfInfo())
1318 | } catch (err) {
1319 | Bot.makeLog("error", [`${this.name}(${this.id}) ${this.version} 连接失败`, err], id)
1320 | return false
1321 | }
1322 |
1323 | Bot[id].sdk.on("message", event => this.makeMessage(id, event))
1324 | Bot[id].sdk.on("notice", event => this.makeNotice(id, event))
1325 |
1326 | Bot.makeLog("mark", `${this.name}(${this.id}) ${this.version} ${Bot[id].nickname} 已连接`, id)
1327 | Bot.em(`connect.${id}`, { self_id: id })
1328 | return true
1329 | }
1330 |
1331 | async makeWebHookSign(id, req, secret) {
1332 | const { sign } = (await import("tweetnacl")).default
1333 | const { plain_token, event_ts } = req.body.d
1334 | while (secret.length < 32)
1335 | secret = secret.repeat(2).slice(0, 32)
1336 | const signature = Buffer.from(sign.detached(
1337 | Buffer.from(`${event_ts}${plain_token}`),
1338 | sign.keyPair.fromSeed(Buffer.from(secret)).secretKey,
1339 | )).toString("hex")
1340 | Bot.makeLog("debug", ["QQBot 签名生成", { plain_token, signature }], id)
1341 | req.res.send({ plain_token, signature })
1342 | }
1343 |
1344 | makeWebHook(req) {
1345 | const appid = req.headers["x-bot-appid"]
1346 | if (!(appid in this.appid))
1347 | return Bot.makeLog("warn", "找不到对应 QQBot", appid)
1348 | if ("plain_token" in req.body?.d)
1349 | return this.makeWebHookSign(this.appid[appid].uin, req, this.appid[appid].info.secret)
1350 | if ("t" in req.body)
1351 | this.appid[appid].sdk.dispatchEvent(req.body.t, req.body)
1352 | req.res.sendStatus(200)
1353 | }
1354 |
1355 | async load() {
1356 | Bot.express.use(`/${this.name}`, this.makeWebHook.bind(this))
1357 | Bot.express.quiet.push(`/${this.name}`)
1358 | for (const token of config.token)
1359 | await Bot.sleep(5000, this.connect(token))
1360 | }
1361 | }
1362 |
1363 | Bot.adapter.push(adapter)
1364 |
1365 | export class QQBotAdapter extends plugin {
1366 | constructor() {
1367 | super({
1368 | name: "QQBotAdapter",
1369 | dsc: "QQBot 适配器设置",
1370 | event: "message",
1371 | rule: [
1372 | {
1373 | reg: "^#[Qq]+[Bb]ot账号$",
1374 | fnc: "List",
1375 | permission: config.permission,
1376 | },
1377 | {
1378 | reg: "^#[Qq]+[Bb]ot设置[0-9]+:[0-9]+:.+:.+:([01]:[01]|2)$",
1379 | fnc: "Token",
1380 | permission: config.permission,
1381 | },
1382 | {
1383 | reg: "^#[Qq]+[Bb]ot[Mm](ark)?[Dd](own)?[0-9]+:",
1384 | fnc: "Markdown",
1385 | permission: config.permission,
1386 | },
1387 | {
1388 | reg: "^#[Qq]+[Bb]ot绑定用户.+$",
1389 | fnc: "BindUser",
1390 | }
1391 | ]
1392 | })
1393 | }
1394 |
1395 | List() {
1396 | this.reply(`共${config.token.length}个账号:\n${config.token.join("\n")}`, true)
1397 | }
1398 |
1399 | async Token() {
1400 | const token = this.e.msg.replace(/^#[Qq]+[Bb]ot设置/, "").trim()
1401 | if (config.token.includes(token)) {
1402 | config.token = config.token.filter(item => item !== token)
1403 | this.reply(`账号已删除,重启后生效,共${config.token.length}个账号`, true)
1404 | } else {
1405 | if (await adapter.connect(token)) {
1406 | config.token.push(token)
1407 | this.reply(`账号已连接,共${config.token.length}个账号`, true)
1408 | } else {
1409 | this.reply(`账号连接失败`, true)
1410 | return false
1411 | }
1412 | }
1413 | await configSave()
1414 | }
1415 |
1416 | async Markdown() {
1417 | let token = this.e.msg.replace(/^#[Qq]+[Bb]ot[Mm](ark)?[Dd](own)?/, "").trim().split(":")
1418 | const bot_id = token.shift()
1419 | token = token.join(":")
1420 | this.reply(`Bot ${bot_id} Markdown 模板已设置为 ${token}`, true)
1421 | config.markdown[bot_id] = token
1422 | await configSave()
1423 | }
1424 |
1425 | BindUser() {
1426 | const id = this.e.msg.replace(/^#[Qq]+[Bb]ot绑定用户(确认)?/, "").trim()
1427 | if (id === this.e.user_id)
1428 | return this.reply("请切换到对应Bot")
1429 |
1430 | adapter.bind_user[this.e.user_id] = id
1431 | this.reply([
1432 | `绑定 ${id} → ${this.e.user_id}`,
1433 | segment.button([{
1434 | text: "确认绑定",
1435 | callback: `#QQBot绑定用户确认${this.e.user_id}`,
1436 | permission: this.e.user_id,
1437 | }])
1438 | ])
1439 | }
1440 | }
1441 |
1442 | logger.info(logger.green("- QQBot 适配器插件 加载完成"))
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "QQBot-Plugin",
3 | "type": "module",
4 | "author": "TimeRainStarSky",
5 | "dependencies": {
6 | "qq-group-bot": "1.1.0",
7 | "qrcode": "^1.5.4",
8 | "sharp": "^0.33.5",
9 | "silk-wasm": "^3.6.3",
10 | "tweetnacl": "^1.0.3",
11 | "url-regex-safe": "^4.0.0"
12 | }
13 | }
--------------------------------------------------------------------------------