├── .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://visitor-badge.glitch.me/badge?page_id=TimeRainStarSky.Yunzai-QQBot-Plugin&right_color=red&left_text=访%20问%20量)](https://github.com/TimeRainStarSky/Yunzai-QQBot-Plugin) 8 | [![Stars](https://img.shields.io/github/stars/TimeRainStarSky/Yunzai-QQBot-Plugin?color=yellow&label=收藏)](../../stargazers) 9 | [![Downloads](https://img.shields.io/github/downloads/TimeRainStarSky/Yunzai-QQBot-Plugin/total?color=blue&label=下载)](../../archive/main.tar.gz) 10 | [![Releases](https://img.shields.io/github/v/release/TimeRainStarSky/Yunzai-QQBot-Plugin?color=green&label=发行版)](../../releases/latest) 11 | 12 | [![访问量](https://profile-counter.glitch.me/TimeRainStarSky-Yunzai-QQBot-Plugin/count.svg)](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 | } --------------------------------------------------------------------------------