├── .gitignore ├── icon.png ├── src ├── lib │ ├── constants.js │ ├── limiter.js │ ├── subprotocol.js │ ├── buffer-util.js │ ├── validation.js │ ├── stream.js │ ├── extension.js │ ├── event-target.js │ ├── sender.js │ ├── permessage-deflate.js │ └── websocket-server.js ├── qqnt │ ├── IpcHandle.js │ ├── api.js │ └── QQNtAPI.js ├── preload.js ├── network │ ├── httpReporter.js │ ├── httpServer.js │ ├── wsReverseServer.js │ └── wsServer.js ├── common │ ├── const.js │ └── setting.html ├── utils.js ├── oneBot11 │ ├── messageModel.js │ ├── event.js │ ├── oneBot11.js │ ├── message.js │ └── api.js ├── logger.js ├── main │ ├── main.js │ └── core.js └── renderer.js ├── manifest.json ├── README.md └── doc ├── notice.md ├── message.md └── http.md /.gitignore: -------------------------------------------------------------------------------- 1 | test/* 2 | .idea/* 3 | node_modules/* -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2891954521/LiteLoaderQQNT-OneBotApi-JS/HEAD/icon.png -------------------------------------------------------------------------------- /src/lib/constants.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | BINARY_TYPES: ['nodebuffer', 'arraybuffer', 'fragments'], 5 | EMPTY_BUFFER: Buffer.alloc(0), 6 | GUID: '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', 7 | kForOnEventAttribute: Symbol('kIsForOnEventAttribute'), 8 | kListener: Symbol('kListener'), 9 | kStatusCode: Symbol('status-code'), 10 | kWebSocket: Symbol('websocket'), 11 | NOOP: () => {} 12 | }; 13 | -------------------------------------------------------------------------------- /src/qqnt/IpcHandle.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 客户端内部消息处理模块 3 | */ 4 | 5 | const Api = require("./api"); 6 | 7 | const handleCmd = new Map() 8 | 9 | for(let item of Api.api){ 10 | for(let key of item.cmdNames){ 11 | handleCmd.set(key, item.handle) 12 | } 13 | } 14 | 15 | /** 16 | * 解析向渲染进程发送的消息 17 | */ 18 | function onMessageHandle(cmdObject){ 19 | handleCmd.get(cmdObject.cmdName)?.(cmdObject); 20 | } 21 | 22 | module.exports = { 23 | onMessageHandle 24 | } -------------------------------------------------------------------------------- /src/preload.js: -------------------------------------------------------------------------------- 1 | const { contextBridge, ipcRenderer } = require("electron"); 2 | 3 | const runtimeData = ipcRenderer.sendSync('one_bot_api_get_runtime_data'); 4 | 5 | const IPCAction = runtimeData['IPCAction']; 6 | // const setting = runtimeData['Setting']; 7 | 8 | contextBridge.exposeInMainWorld("OneBotApi", { 9 | 10 | ipcRenderer_OneBot: ipcRenderer, 11 | 12 | isDebug: runtimeData['isDebug'], 13 | 14 | IPCAction: IPCAction, 15 | 16 | send: (action, data) => ipcRenderer.send(action, data), 17 | 18 | invoke: (action) => ipcRenderer.invoke(action), 19 | 20 | settingData: () => ipcRenderer.invoke(IPCAction.ACTION_GET_CONFIG) 21 | }); -------------------------------------------------------------------------------- /src/network/httpReporter.js: -------------------------------------------------------------------------------- 1 | const { Log } = require("../logger"); 2 | const { Setting, Reporter } = require('../main/core'); 3 | 4 | function report(data){ 5 | try{ 6 | if(data == null) return; 7 | data = JSON.stringify(data) 8 | fetch(Setting.setting.http.host, { 9 | method: "POST", 10 | headers: { "Content-Type": "application/json" }, 11 | body: data 12 | }).then(() => {}, (err) => { 13 | Log.w(`http report fail: ${err}\n${data}`); 14 | }); 15 | }catch(e){ 16 | Log.e(e.toString()); 17 | } 18 | } 19 | 20 | 21 | function startHttpReport(){ 22 | Reporter.httpReporter = report; 23 | } 24 | 25 | function stopHttpReport(){ 26 | Reporter.httpReporter = null; 27 | } 28 | 29 | 30 | module.exports = { 31 | startHttpReport, 32 | stopHttpReport 33 | } -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 4, 3 | "type": "extension", 4 | "name": "OneBotApi-JS", 5 | "slug": "OneBotApi-JS", 6 | "description": "在LiteLoaderQQNT上添加OneBot协议支持", 7 | "version": "0.2.10", 8 | "thumbnail": "./icon.png", 9 | "authors": [{ 10 | "name": "GreenDog", 11 | "link": "https://github.com/2891954521/LiteLoaderQQNT-OneBotApi-JS" 12 | }], 13 | "repository": { 14 | "repo": "2891954521/LiteLoaderQQNT-OneBotApi-JS", 15 | "branch": "main", 16 | "use_release": { 17 | "tag": "latest", 18 | "name": "release.zip" 19 | } 20 | }, 21 | "platform": [ 22 | "win32", 23 | "linux", 24 | "darwin" 25 | ], 26 | "injects": { 27 | "renderer": "./src/renderer.js", 28 | "main": "./src/main/main.js", 29 | "preload": "./src/preload.js" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/lib/limiter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const kDone = Symbol('kDone'); 4 | const kRun = Symbol('kRun'); 5 | 6 | /** 7 | * A very simple job queue with adjustable concurrency. Adapted from 8 | * https://github.com/STRML/async-limiter 9 | */ 10 | class Limiter { 11 | /** 12 | * Creates a new `Limiter`. 13 | * 14 | * @param {Number} [concurrency=Infinity] The maximum number of jobs allowed 15 | * to run concurrently 16 | */ 17 | constructor(concurrency) { 18 | this[kDone] = () => { 19 | this.pending--; 20 | this[kRun](); 21 | }; 22 | this.concurrency = concurrency || Infinity; 23 | this.jobs = []; 24 | this.pending = 0; 25 | } 26 | 27 | /** 28 | * Adds a job to the queue. 29 | * 30 | * @param {Function} job The job to run 31 | * @public 32 | */ 33 | add(job) { 34 | this.jobs.push(job); 35 | this[kRun](); 36 | } 37 | 38 | /** 39 | * Removes a job from the queue and runs it if possible. 40 | * 41 | * @private 42 | */ 43 | [kRun]() { 44 | if (this.pending === this.concurrency) return; 45 | 46 | if (this.jobs.length) { 47 | const job = this.jobs.shift(); 48 | 49 | this.pending++; 50 | job(this[kDone]); 51 | } 52 | } 53 | } 54 | 55 | module.exports = Limiter; 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OneBot API JS 2 | 3 | 在 LiteLoaderQQNT 上添加 OneBot11 协议支持 4 | 5 | 项目灵感来源于[LiteLoaderQQNT-OneBotApi](https://github.com/linyuchen/LiteLoaderQQNT-OneBotApi),本项目为JS实现 6 | 7 | ## 关于 8 | - 本项目为轻量级框架,仅实现了部分核心功能,可能不支持现有的QQ机器人框架 9 | - QQ版本仅在 Windows 下 `9.9.12.25765`([点击下载](https://dldir1.qq.com/qqfile/qq/QQNT/960a88c0/QQ9.9.12.25765_x64.exe)) 测试运行通过 10 | - 框架功能仍在完善中,如在使用时遇到问题或有任何希望改进的地方欢迎提交 Issue 11 | 12 | ## 安装方法 13 | 1. 安装 [LiteLoaderQQNT](https://github.com/LiteLoaderQQNT/LiteLoaderQQNT) 1.0.0 及以上版本 14 | 2. 下载本项目插件 [LiteLoaderQQNT-OneBotApi-JS](https://github.com/2891954521/LiteLoaderQQNT-OneBotApi-JS/releases) 并手动放置在LiteLoaderQQNT插件目录下:`./LiteLoaderQQNT/plugins` 15 | 16 | 17 | ## API文档 18 | 详见 [OneBot11](https://github.com/botuniverse/onebot-11),所有已实现的 API 如无特殊说明均以 OneBot11 文档为准 19 | 20 | - **支持的通信方式** 21 | - HTTP API 和 上报 22 | - 正向 WebSocket 23 | - 反向 WebSocket 24 | 25 | - **支持的API** 26 | - 收发消息、频道消息、撤回消息、获取好友/群列表等 27 | - 详见 [HTTP API](https://github.com/2891954521/LiteLoaderQQNT-OneBotApi-JS/blob/main/doc/http.md) 28 | 29 | - **支持的消息类型** 30 | - 支持常见消息类型:文本、表情、图片、At、回复、撤回等 31 | - 详见 [Message](https://github.com/2891954521/LiteLoaderQQNT-OneBotApi-JS/blob/main/doc/message.md) 和 [Notice](https://github.com/2891954521/LiteLoaderQQNT-OneBotApi-JS/blob/main/doc/notice.md) -------------------------------------------------------------------------------- /src/lib/subprotocol.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { tokenChars } = require('./validation'); 4 | 5 | /** 6 | * Parses the `Sec-WebSocket-Protocol` header into a set of subprotocol names. 7 | * 8 | * @param {String} header The field value of the header 9 | * @return {Set} The subprotocol names 10 | * @public 11 | */ 12 | function parse(header) { 13 | const protocols = new Set(); 14 | let start = -1; 15 | let end = -1; 16 | let i = 0; 17 | 18 | for (i; i < header.length; i++) { 19 | const code = header.charCodeAt(i); 20 | 21 | if (end === -1 && tokenChars[code] === 1) { 22 | if (start === -1) start = i; 23 | } else if ( 24 | i !== 0 && 25 | (code === 0x20 /* ' ' */ || code === 0x09) /* '\t' */ 26 | ) { 27 | if (end === -1 && start !== -1) end = i; 28 | } else if (code === 0x2c /* ',' */) { 29 | if (start === -1) { 30 | throw new SyntaxError(`Unexpected character at index ${i}`); 31 | } 32 | 33 | if (end === -1) end = i; 34 | 35 | const protocol = header.slice(start, end); 36 | 37 | if (protocols.has(protocol)) { 38 | throw new SyntaxError(`The "${protocol}" subprotocol is duplicated`); 39 | } 40 | 41 | protocols.add(protocol); 42 | start = end = -1; 43 | } else { 44 | throw new SyntaxError(`Unexpected character at index ${i}`); 45 | } 46 | } 47 | 48 | if (start === -1 || end !== -1) { 49 | throw new SyntaxError('Unexpected end of input'); 50 | } 51 | 52 | const protocol = header.slice(start, i); 53 | 54 | if (protocols.has(protocol)) { 55 | throw new SyntaxError(`The "${protocol}" subprotocol is duplicated`); 56 | } 57 | 58 | protocols.add(protocol); 59 | return protocols; 60 | } 61 | 62 | module.exports = { parse }; 63 | -------------------------------------------------------------------------------- /doc/notice.md: -------------------------------------------------------------------------------- 1 | # 通知消息 2 | 3 | ### 支持的消息类型 4 | 5 | | 消息类型 | 接收 | 备注 | 6 | |--------|:--:|----| 7 | | 好友请求 | ✔ | | | 8 | | 好友消息撤回 | ✔ | | | 9 | | 群消息撤回 | ✔ | | | 10 | | 群成员减少 | ✔ | | | 11 | | 群成员增加 | ✔ | | | 12 | 13 | ## 消息结构 14 | 15 | ### 收到好友请求 16 | ```json lines 17 | { 18 | "request_type": "friend", 19 | "user_id": "123456", // 好友QQ号 20 | "comment": "我是xxx", // 加好友的验证消息 21 | "flag": "xxxx" // 处理好友请求的flag 22 | } 23 | ``` 24 | 25 | ### 好友消息撤回 26 | ```json lines 27 | { 28 | "time": 1708072121, 29 | "self_id": "123456", 30 | 31 | "post_type": "notice", 32 | "notice_type": "friend_recall", 33 | 34 | "user_id": "123456", 35 | "operator_id": "123456", 36 | "message_id": "xxxxxx", 37 | } 38 | ``` 39 | 40 | ### 群消息撤回 41 | ```json lines 42 | { 43 | "time": 1708072121, 44 | "self_id": "123456", 45 | 46 | "post_type": "notice", 47 | "notice_type": "group_recall", 48 | 49 | "user_id": "123456", 50 | "group_id": "123456", 51 | "operator_id": "123456", 52 | "message_id": "xxxxxx", 53 | } 54 | ``` 55 | 56 | ### 群成员减少 57 | ```json lines 58 | { 59 | "time": 1708072121, 60 | "self_id": "123456", 61 | 62 | "post_type": "notice", 63 | "notice_type": "group_decrease", 64 | "sub_type": "leave", 65 | 66 | "user_id": "123456", 67 | "group_id": "123456", 68 | "operator_id": "123456" 69 | } 70 | ``` 71 | 72 | ### 群成员增加 73 | ```json lines 74 | { 75 | "time": 1708072121, 76 | "self_id": "123456", 77 | 78 | "post_type": "notice", 79 | "notice_type": "group_increase", 80 | "sub_type": "invite", 81 | 82 | "user_id": "123456", 83 | "group_id": "123456", 84 | "operator_id": "123456" 85 | } 86 | ``` -------------------------------------------------------------------------------- /src/common/const.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | IPCAction: { 3 | /** 4 | * 获取配置信息 5 | */ 6 | ACTION_GET_CONFIG: "one_bot_api_get_config", 7 | /** 8 | * 保存配置信息 9 | */ 10 | ACTION_SET_CONFIG: "one_bot_api_set_config", 11 | 12 | ACTION_LOG: "one_bot_api_log", 13 | 14 | /** 15 | * 获取好友列表 16 | */ 17 | ACTION_GET_FRIENDS: "one_bot_api_get_friends", 18 | 19 | /** 20 | * 获取群列表 21 | */ 22 | ACTION_GET_GROUPS: "one_bot_api_get_groups", 23 | 24 | /** 25 | * 主界面加载 26 | */ 27 | ACTION_LOAD_MAIN_PAGE: "one_bot_api_load_main_page", 28 | 29 | /** 30 | * 重启HTTP服务 31 | */ 32 | ACTION_RESTART_HTTP_SERVER: "one_bot_api_restart_http_server", 33 | 34 | /** 35 | * 重启ws服务 36 | */ 37 | ACTION_RESTART_WS_SERVER: "one_bot_api_restart_ws_server", 38 | 39 | /** 40 | * 关闭ws服务 41 | */ 42 | ACTION_STOP_WS_SERVER: "one_bot_api_stop_ws_server", 43 | 44 | /** 45 | * 重启ws reverse服务 46 | */ 47 | ACTION_RESTART_WS_REVERSE_SERVER: "one_bot_api_restart_ws_reverse_server", 48 | 49 | /** 50 | * 关闭ws reverse服务 51 | */ 52 | ACTION_STOP_WS_REVERSE_SERVER: "one_bot_api_stop_ws_reverse_server", 53 | 54 | /** 55 | * 获取服务运行状态 56 | */ 57 | ACTION_SERVER_STATUS: 'one_bot_api_server_status', 58 | 59 | /** 60 | * API测试 61 | */ 62 | ACTION_HTTP_TEST: 'one_bot_api_http_test', 63 | }, 64 | 65 | // 默认设置文件 66 | defaultSetting: { 67 | "http": { 68 | 'enableServer': true, 69 | "port": 5000, 70 | 71 | "host": "http://127.0.0.1:8080/", 72 | 73 | 'enableReport': true, 74 | "hosts": ["http://127.0.0.1:8080/"] 75 | }, 76 | 77 | "ws": { 78 | 'enable': false, 79 | 'host': '0.0.0.0', 80 | "port": 5001, 81 | }, 82 | 83 | "wsReverse": { 84 | enable: false, 85 | url: '', 86 | apiUrl: '', 87 | eventUrl: '', 88 | useUniversalClient: false, 89 | reconnectInterval: 3000 90 | }, 91 | 92 | "setting": { 93 | "reportSelfMsg": true, 94 | "reportOldMsg": false, 95 | "autoAcceptFriendRequest": false 96 | }, 97 | 98 | "misc": { 99 | 'disableUpdate': true 100 | }, 101 | 102 | "debug": { 103 | "debug": false, 104 | "ipc": false, 105 | } 106 | } 107 | } -------------------------------------------------------------------------------- /src/network/httpServer.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const querystring = require('querystring'); 3 | 4 | const { Log } = require('../logger'); 5 | const { oneBot11API} = require("../oneBot11/oneBot11"); 6 | 7 | 8 | let errorMsg = null; 9 | 10 | let isRunning = false; 11 | 12 | const server = http.createServer(async (req, res) => { 13 | if(req.method !== 'POST'){ 14 | res.statusCode = 200; 15 | res.setHeader('Content-Type', 'text/plain'); 16 | res.end('Http server is running'); 17 | return; 18 | } 19 | 20 | let body = ''; 21 | req.on('data', (chunk) => { body += chunk; }); 22 | req.on('end', async() => { 23 | res.statusCode = 200; 24 | res.setHeader('Content-Type', 'application/json'); 25 | 26 | try{ 27 | let contentType = req.headers['content-type']; 28 | let form; 29 | 30 | if(contentType === "application/json"){ 31 | form = body !== "" ? JSON.parse(body) : {} 32 | 33 | }else if(contentType === "application/x-www-form-urlencoded"){ 34 | form = querystring.parse(body); 35 | 36 | }else if(contentType === "multipart/form-data"){ 37 | res.end('{ "code": 403, "msg": "Unsupport content type" }'); 38 | return; 39 | 40 | }else if(body.length > 0){ 41 | res.end('{ "code": 400, "msg": "Wrong content type" }'); 42 | return; 43 | 44 | }else{ 45 | form = { }; 46 | } 47 | 48 | const handler = oneBot11API[req.url[0] == '/' ? req.url.slice(1) : req.url]; 49 | if(handler){ 50 | res.end(JSON.stringify(await handler(form))); 51 | }else{ 52 | res.end('{ "code": 404, "msg": "Not Found" }'); 53 | } 54 | }catch(error){ 55 | Log.e(error); 56 | res.end(`{ "code": 500, "msg": ${error.toString()} }`); 57 | } 58 | }); 59 | 60 | }); 61 | 62 | 63 | function startHttpServer(port){ 64 | if(isRunning) return; 65 | 66 | server.on('error', (e) => { 67 | if(e.code === 'EADDRINUSE'){ 68 | errorMsg = "端口已被占用"; 69 | Log.w(`Port ${port} is already in used`); 70 | } 71 | }); 72 | 73 | server.listen(port, '0.0.0.0', () => { 74 | isRunning = true; 75 | Log.i(`HTTP Server running at http://0.0.0.0:${port}/`); 76 | }); 77 | } 78 | 79 | 80 | async function restartHttpServer(port){ 81 | if(isRunning){ 82 | await stopHttpServer(); 83 | Log.i(`restarting Http Server.`); 84 | startHttpServer(port) 85 | }else{ 86 | startHttpServer(port); 87 | } 88 | } 89 | 90 | 91 | function stopHttpServer(){ 92 | return new Promise((resolve) => { 93 | if(isRunning){ 94 | server.close(() => { 95 | Log.i(`HTTP Server stopped.`); 96 | isRunning = false; 97 | resolve(); 98 | }) 99 | }else{ 100 | resolve(); 101 | } 102 | }) 103 | } 104 | 105 | 106 | module.exports = { 107 | getStatus: () => { 108 | return { 109 | status: isRunning, 110 | msg: errorMsg 111 | } 112 | }, 113 | 114 | startHttpServer, 115 | restartHttpServer, 116 | stopHttpServer 117 | } -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require('path'); 3 | const crypto = require("crypto"); 4 | 5 | let settingsPath = './'; 6 | 7 | 8 | /** 9 | * 下载文件 10 | * @param {string} uri 11 | * @param {string} savePath - 保存的路径 12 | */ 13 | async function downloadFile(uri, savePath){ 14 | let url = new URL(uri); 15 | if(url.protocol === "base64:"){ 16 | fs.writeFileSync(savePath, Buffer.from(uri.split("base64://")[1], 'base64')); 17 | }else if(url.protocol === "http:" || url.protocol === "https:"){ 18 | let res = await fetch(url) 19 | let blob = await res.blob(); 20 | let buffer = await blob.arrayBuffer(); 21 | fs.writeFileSync(savePath, Buffer.from(buffer)); 22 | } 23 | } 24 | 25 | 26 | /** 27 | * 计算文件 md5 28 | * @param {string} filePath 29 | * @return {string} 30 | */ 31 | function md5(filePath){ 32 | const hash = crypto.createHash('md5'); 33 | hash.update(fs.readFileSync(filePath)); 34 | return hash.digest('hex'); 35 | } 36 | 37 | async function wait(ms){ 38 | return new Promise(resolve => setTimeout(() => resolve(), ms)); 39 | } 40 | 41 | /** 42 | * 从本地文件加载设置信息 43 | * @param plugin 44 | */ 45 | function loadSetting(plugin){ 46 | const pluginDataPath = plugin.path.data; 47 | 48 | settingsPath = path.join(pluginDataPath, "settings.json"); 49 | 50 | // 设置文件是否存在判断 51 | if(!fs.existsSync(pluginDataPath)){ 52 | fs.mkdirSync(pluginDataPath, { recursive: true }); 53 | } 54 | 55 | if(!fs.existsSync(settingsPath)){ 56 | return null; 57 | }else{ 58 | return JSON.parse(fs.readFileSync(settingsPath, "utf-8")) 59 | } 60 | } 61 | 62 | function saveSetting(content){ 63 | const new_config = typeof content == "string" ? JSON.stringify(JSON.parse(content), null, 4) : JSON.stringify(content, null, 4) 64 | fs.writeFileSync(settingsPath, new_config, "utf-8"); 65 | } 66 | 67 | 68 | function logToFile(msg){ 69 | let currentDateTime = new Date().toLocaleString(); 70 | fs.appendFile("./onebotapi.log", currentDateTime + ":" + msg + "\n", (err) => { }); 71 | } 72 | 73 | 74 | function checkAndCompleteKeys(json1, json2){ 75 | // 补全缺少的 key 76 | const keys1 = Object.keys(json1); 77 | const keys2 = Object.keys(json2); 78 | for(const key of keys2){ 79 | if(keys1.includes(key)){ 80 | const keys3 = Object.keys(json1[key]); 81 | for(const key1 of Object.keys(json2[key])){ 82 | if(!keys3.includes(key1)) json1[key][key1] = json2[key][key1]; 83 | } 84 | }else{ 85 | json1[key] = json2[key]; 86 | } 87 | } 88 | return json1; 89 | } 90 | 91 | 92 | class LimitedHashMap{ 93 | constructor(capacity){ 94 | this.capacity = capacity; 95 | this.map = {}; 96 | this.keys = []; 97 | } 98 | 99 | put(key, value){ 100 | // 如果键不存在,检查容量是否达到上限 101 | if(this.keys.length >= this.capacity){ 102 | // 移除最早的数据 103 | const oldestKey = this.keys.shift(); 104 | delete this.map[oldestKey]; 105 | } 106 | // 添加新数据 107 | this.map[key] = value; 108 | this.keys.push(key); 109 | } 110 | 111 | get(key){ 112 | if(this.map.hasOwnProperty(key)){ 113 | return this.map[key]; 114 | }else{ 115 | return null; 116 | } 117 | } 118 | } 119 | 120 | module.exports = { 121 | md5, 122 | wait, 123 | downloadFile, 124 | 125 | logToFile, 126 | checkAndCompleteKeys, 127 | 128 | loadSetting, 129 | saveSetting, 130 | 131 | LimitedHashMap, 132 | } -------------------------------------------------------------------------------- /src/oneBot11/messageModel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 消息处理模块 3 | */ 4 | 5 | const { Log } = require('../logger'); 6 | const { QQNtAPI } = require('../qqnt/QQNtAPI'); 7 | const { Data, Setting, Reporter } = require('../main/core'); 8 | 9 | const { createPeer, createOneBot, Text} = require("./message"); 10 | 11 | const Event = require("./event") 12 | 13 | 14 | /** 15 | * 发送消息 16 | * @param postData 17 | */ 18 | async function sendMessage(postData){ 19 | let peer = createPeer(postData.group_id, postData.user_id); 20 | 21 | if(!peer){ 22 | return { msg: postData.group_id ? `找不到群 (${postData.group_id})` : `找不到好友 (${postData.user_id})`} 23 | } 24 | 25 | let message; 26 | if(postData.message.constructor === String){ 27 | message = [ new Text(postData.message) ] 28 | }else{ 29 | message = [] 30 | for(let item of postData.message){ 31 | message.push(await createOneBot(item, postData.group_id)) 32 | } 33 | } 34 | 35 | let oneBotMsg = new Event.MessageEvent(0, "", postData.user_id, message) 36 | 37 | let qqNtMsg = await QQNtAPI.sendMessage(peer, oneBotMsg.message.map((item) => item.toQQNT())); 38 | 39 | oneBotMsg.time = parseInt(qqNtMsg?.msgTime || 0); 40 | oneBotMsg.message_id = qqNtMsg.msgId; 41 | if(postData.group_id){ 42 | oneBotMsg.message_type = "group"; 43 | oneBotMsg.sub_type = "normal"; 44 | oneBotMsg.group_id = postData.group_id; 45 | }else{ 46 | oneBotMsg.message_type = "private"; 47 | oneBotMsg.sub_type = "friend"; 48 | } 49 | if(postData.group_id) Log.i(`发送群 (${postData.group_id}) 消息:${oneBotMsg.raw_message}`); 50 | else Log.i(`发送好友 (${postData.user_id}) 消息:${oneBotMsg.raw_message}`); 51 | 52 | Data.pushHistoryMessage(qqNtMsg, oneBotMsg); 53 | 54 | return { data: { message_id: qqNtMsg.msgId } }; 55 | } 56 | 57 | /** 58 | * 处理新消息 59 | */ 60 | async function handleNewMessage(messages){ 61 | for(/** @type QQNTMessage */ let message of messages){ 62 | let oneBotMsg = await Event.parseMessage(message) 63 | if(oneBotMsg){ 64 | switch(oneBotMsg.eventType){ 65 | case 1: 66 | Log.i(`收到好友 (${oneBotMsg.user_id}) 的消息:${oneBotMsg.raw_message}`); break; 67 | case 2: 68 | Log.i(`收到群 (${oneBotMsg.group_id}) 内 (${oneBotMsg.user_id}) 的消息:${oneBotMsg.raw_message}`); break; 69 | case 4: 70 | Log.i(`收到频道 (${oneBotMsg.guild_id}/${oneBotMsg.channel_id}) 内 (${oneBotMsg.tiny_id}) 的消息:${oneBotMsg.raw_message}`); break; 71 | } 72 | 73 | Data.pushHistoryMessage(message, oneBotMsg); 74 | 75 | if(oneBotMsg.user_id != oneBotMsg.self_id || Setting.setting.setting.reportSelfMsg){ 76 | Reporter.reportData(oneBotMsg); 77 | } 78 | }else{ 79 | Log.w(`解析消息失败, 消息内容: ${JSON.stringify(messages)}`) 80 | } 81 | } 82 | } 83 | 84 | async function recallMessage(message){ 85 | let recall = await Event.RecallMessage.parseFromQQNT(message) 86 | if(recall.group_id) Log.i(`群 (${recall.group_id}) 内 (${recall.user_id}) 撤回了一条消息`); 87 | else Log.i(`好友 (${recall.user_id}) 撤回了一条消息`); 88 | Reporter.reportData(recall); 89 | } 90 | 91 | 92 | /** 93 | * 发送通知上报 94 | * @param {*} postData 95 | */ 96 | function postNoticeData(postData){ 97 | if(!postData.time) postData.time = 0; 98 | postData['self_id'] = Data.selfInfo.uin; 99 | postData['post_type'] = "notice"; 100 | Reporter.reportData(postData); 101 | } 102 | 103 | 104 | /** 105 | * 发送通知上报 106 | * @param {*} postData 107 | */ 108 | function postRequestData(postData){ 109 | postData['time'] = 0; 110 | postData['self_id'] = Data.selfInfo.uin; 111 | postData['post_type'] = "request"; 112 | Reporter.reportData(postData); 113 | } 114 | 115 | 116 | module.exports = { 117 | sendMessage, 118 | 119 | handleNewMessage, 120 | recallMessage, 121 | 122 | postNoticeData, 123 | postRequestData, 124 | } -------------------------------------------------------------------------------- /src/network/wsReverseServer.js: -------------------------------------------------------------------------------- 1 | const WebSocket = require('../lib/websocket'); 2 | const { Log } = require("../logger"); 3 | const { Reporter, Data} = require('../main/core'); 4 | const { oneBot11API} = require('../oneBot11/oneBot11') 5 | 6 | class WebSocketClient{ 7 | 8 | constructor(name, handleMsg, role){ 9 | this.ws = null; 10 | this.error = null; 11 | this.isOpen = false; 12 | 13 | this.name = name; 14 | this.role = role; 15 | this.handleMsg = handleMsg; 16 | } 17 | 18 | openSocket(url){ 19 | try{ 20 | this.ws = new WebSocket(url, { 21 | headers: { 22 | 'X-Self-ID': Data.selfInfo.uin, 23 | 'X-Client-Role': this.role 24 | } 25 | }); 26 | 27 | this.ws.on('open', () => { 28 | Log.i(`${this.name} is connected`); 29 | this.error = null; 30 | this.isOpen = true; 31 | if(this.handleMsg) this.ws.on('message', (data) => handle(this.ws, data)); 32 | }); 33 | 34 | this.ws.on('error', (e) => { 35 | Log.e(e.stack); 36 | this.error = e.stack; 37 | this.isOpen = false; 38 | }); 39 | 40 | this.ws.on('close', (code) => { 41 | if(code != 1000){ 42 | Log.w(`${this.name} was closed with code ${code}`); 43 | this.error = '连接意外关闭'; 44 | } 45 | this.ws = null; 46 | this.isOpen = false; 47 | }); 48 | }catch(e){ 49 | Log.e(e.stack); 50 | this.error = e.stack; 51 | } 52 | } 53 | 54 | stopSocket(){ 55 | if(this.ws && this.isOpen) this.ws.close(); 56 | this.ws = null; 57 | this.error = null; 58 | this.isOpen = false; 59 | } 60 | } 61 | 62 | 63 | let wss = new WebSocketClient('ws', true, 'Universal'); 64 | let api = new WebSocketClient('api', false, 'API'); 65 | let event = new WebSocketClient('event', true, 'Event'); 66 | 67 | function handle(ws, data){ 68 | try{ 69 | let params = JSON.parse(data); 70 | const handler = oneBot11API[params?.action]; 71 | if(handler){ 72 | new Promise(async(resolve) => { 73 | resolve(await handler(params.params || {})) 74 | }).then((result) => { 75 | result.echo = params.echo; 76 | ws.send(JSON.stringify(result)); 77 | }, (err) => { 78 | Log.e(err.stack); 79 | }); 80 | }else{ 81 | ws.send(`{"status": "failed", "retcode": 1404, "data": null, "echo": "${params.echo}"}`) 82 | } 83 | }catch(e){ 84 | Log.e(e.stack); 85 | } 86 | } 87 | 88 | 89 | function report(data){ 90 | try{ 91 | wss.ws?.send(data); 92 | event.ws?.send(data); 93 | }catch(e){ 94 | Log.e(e.stack); 95 | } 96 | } 97 | 98 | 99 | function startWsClient(params){ 100 | Log.i(`Starting WebSocket Client.`); 101 | if(params.url && !wss.ws) wss.openSocket(params.url); 102 | if(params.apiUrl && !api.ws) api.openSocket(params.apiUrl); 103 | if(params.eventUrl && !event.ws) event.openSocket(params.eventUrl); 104 | if(wss.ws || event.ws) Reporter.webSocketReverseReporter = report; 105 | } 106 | 107 | 108 | function restartWsClient(params){ 109 | Log.i(`Restarting WebSocket Client.`); 110 | stopWsClient(); 111 | startWsClient(params); 112 | } 113 | 114 | 115 | function stopWsClient(){ 116 | Log.i(`Stopping WebSocket Client.`); 117 | Reporter.webSocketReverseReporter = null; 118 | wss.stopSocket(); 119 | api.stopSocket(); 120 | event.stopSocket(); 121 | } 122 | 123 | 124 | module.exports = { 125 | getStatus: () => { 126 | return { 127 | "wss": { 128 | status: wss.ws != null && wss.error == null, 129 | msg: wss.error 130 | }, 131 | "api": { 132 | status: api.ws != null && api.error == null, 133 | msg: api.error 134 | }, 135 | "event": { 136 | status: event.ws != null && event.error == null, 137 | msg: event.error 138 | } 139 | } 140 | }, 141 | 142 | startWsClient, 143 | restartWsClient, 144 | stopWsClient 145 | } -------------------------------------------------------------------------------- /src/lib/buffer-util.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { EMPTY_BUFFER } = require('./constants'); 4 | 5 | const FastBuffer = Buffer[Symbol.species]; 6 | 7 | /** 8 | * Merges an array of buffers into a new buffer. 9 | * 10 | * @param {Buffer[]} list The array of buffers to concat 11 | * @param {Number} totalLength The total length of buffers in the list 12 | * @return {Buffer} The resulting buffer 13 | * @public 14 | */ 15 | function concat(list, totalLength) { 16 | if (list.length === 0) return EMPTY_BUFFER; 17 | if (list.length === 1) return list[0]; 18 | 19 | const target = Buffer.allocUnsafe(totalLength); 20 | let offset = 0; 21 | 22 | for (let i = 0; i < list.length; i++) { 23 | const buf = list[i]; 24 | target.set(buf, offset); 25 | offset += buf.length; 26 | } 27 | 28 | if (offset < totalLength) { 29 | return new FastBuffer(target.buffer, target.byteOffset, offset); 30 | } 31 | 32 | return target; 33 | } 34 | 35 | /** 36 | * Masks a buffer using the given mask. 37 | * 38 | * @param {Buffer} source The buffer to mask 39 | * @param {Buffer} mask The mask to use 40 | * @param {Buffer} output The buffer where to store the result 41 | * @param {Number} offset The offset at which to start writing 42 | * @param {Number} length The number of bytes to mask. 43 | * @public 44 | */ 45 | function _mask(source, mask, output, offset, length) { 46 | for (let i = 0; i < length; i++) { 47 | output[offset + i] = source[i] ^ mask[i & 3]; 48 | } 49 | } 50 | 51 | /** 52 | * Unmasks a buffer using the given mask. 53 | * 54 | * @param {Buffer} buffer The buffer to unmask 55 | * @param {Buffer} mask The mask to use 56 | * @public 57 | */ 58 | function _unmask(buffer, mask) { 59 | for (let i = 0; i < buffer.length; i++) { 60 | buffer[i] ^= mask[i & 3]; 61 | } 62 | } 63 | 64 | /** 65 | * Converts a buffer to an `ArrayBuffer`. 66 | * 67 | * @param {Buffer} buf The buffer to convert 68 | * @return {ArrayBuffer} Converted buffer 69 | * @public 70 | */ 71 | function toArrayBuffer(buf) { 72 | if (buf.length === buf.buffer.byteLength) { 73 | return buf.buffer; 74 | } 75 | 76 | return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.length); 77 | } 78 | 79 | /** 80 | * Converts `data` to a `Buffer`. 81 | * 82 | * @param {*} data The data to convert 83 | * @return {Buffer} The buffer 84 | * @throws {TypeError} 85 | * @public 86 | */ 87 | function toBuffer(data) { 88 | toBuffer.readOnly = true; 89 | 90 | if (Buffer.isBuffer(data)) return data; 91 | 92 | let buf; 93 | 94 | if (data instanceof ArrayBuffer) { 95 | buf = new FastBuffer(data); 96 | } else if (ArrayBuffer.isView(data)) { 97 | buf = new FastBuffer(data.buffer, data.byteOffset, data.byteLength); 98 | } else { 99 | buf = Buffer.from(data); 100 | toBuffer.readOnly = false; 101 | } 102 | 103 | return buf; 104 | } 105 | 106 | module.exports = { 107 | concat, 108 | mask: _mask, 109 | toArrayBuffer, 110 | toBuffer, 111 | unmask: _unmask 112 | }; 113 | 114 | /* istanbul ignore else */ 115 | // if (!process.env.WS_NO_BUFFER_UTIL) { 116 | // try { 117 | // const bufferUtil = require('bufferutil'); 118 | // 119 | // module.exports.mask = function (source, mask, output, offset, length) { 120 | // if (length < 48) _mask(source, mask, output, offset, length); 121 | // else bufferUtil.mask(source, mask, output, offset, length); 122 | // }; 123 | // 124 | // module.exports.unmask = function (buffer, mask) { 125 | // if (buffer.length < 32) _unmask(buffer, mask); 126 | // else bufferUtil.unmask(buffer, mask); 127 | // }; 128 | // } catch (e) { 129 | // // Continue regardless of the error. 130 | // } 131 | // } 132 | -------------------------------------------------------------------------------- /src/network/wsServer.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | 3 | const WebSocketServer = require('../lib/websocket-server'); 4 | const { Log } = require("../logger"); 5 | const { Reporter } = require('../main/core'); 6 | const { oneBot11API} = require('../oneBot11/oneBot11') 7 | 8 | const WsStatus = { 9 | CONNECTING: 0, 10 | OPEN: 1, 11 | CLOSING: 2, 12 | CLOSED: 3 13 | }; 14 | 15 | 16 | let errorMsg = null; 17 | 18 | let isRunning = false; 19 | 20 | const wss = new WebSocketServer({ noServer: true }); 21 | const api = new WebSocketServer({ noServer: true }); 22 | const event = new WebSocketServer({ noServer: true }); 23 | 24 | const server = http.createServer(); 25 | 26 | 27 | function init(){ 28 | wss.on('connection', (ws) => { 29 | ws.on('error', Log.w); 30 | ws.on('message', (data) => handle(ws, data)); 31 | }); 32 | api.on('connection', (ws) => { 33 | ws.on('error', Log.w); 34 | ws.on('message', (data) => handle(ws, data)); 35 | }); 36 | event.on('connection', (ws) => { 37 | ws.on('error', Log.w); 38 | }); 39 | wss.on('error', (err) => Log.w(err)); 40 | api.on('error', (err) => Log.w(err)); 41 | event.on('error', (err) => Log.w(err)); 42 | } 43 | 44 | 45 | function handle(ws, data){ 46 | try{ 47 | let params = JSON.parse(data); 48 | const handler = oneBot11API[params?.action]; 49 | if(handler){ 50 | new Promise(async(resolve) => { 51 | resolve(await handler(params.params || {})) 52 | }).then((result) => { 53 | result.echo = params.echo; 54 | ws.send(JSON.stringify(result)); 55 | }, (err) => { 56 | Log.e(err.stack); 57 | }); 58 | }else{ 59 | ws.send(`{"status": "failed", "retcode": 1404, "data": null, "echo": "${params.echo}"}`) 60 | } 61 | }catch(e){ 62 | Log.e(e.stack); 63 | } 64 | } 65 | 66 | 67 | function report(data){ 68 | wss.clients.forEach((client) => { 69 | if(client.readyState === WsStatus.OPEN) client.send(data); 70 | }); 71 | event.clients.forEach((client) => { 72 | if(client.readyState === WsStatus.OPEN) client.send(data); 73 | }); 74 | } 75 | 76 | 77 | function startWsServer(port){ 78 | if(isRunning) return; 79 | 80 | server.on('upgrade', function upgrade(request, socket, head) { 81 | if(request.url == '/'){ 82 | wss.handleUpgrade(request, socket, head, (ws) => wss.emit('connection', ws, request)); 83 | }else if(request.url == '/api' || request.url == '/api/'){ 84 | api.handleUpgrade(request, socket, head, (ws) => api.emit('connection', ws, request)); 85 | }else if(request.url == '/event' || request.url == '/event/'){ 86 | event.handleUpgrade(request, socket, head, (ws) => event.emit('connection', ws, request)); 87 | }else{ 88 | socket.write('HTTP/1.1 404 Not Found\r\n\r\n'); 89 | socket.destroy(); 90 | } 91 | }); 92 | 93 | server.on('error', (e) => { 94 | if(e.code === 'EADDRINUSE'){ 95 | errorMsg = "端口已被占用"; 96 | Log.w(`Port ${port} is already in used`); 97 | } 98 | }); 99 | 100 | server.listen(port, '0.0.0.0', () => { 101 | isRunning = true; 102 | Reporter.webSocketReporter = report; 103 | Log.i(`WebSocket Server running at http://0.0.0.0:${port}/`); 104 | }); 105 | } 106 | 107 | 108 | async function restartWsServer(port){ 109 | if(isRunning){ 110 | await stopWsServer(); 111 | Log.i(`restarting WebSocket Server.`); 112 | startWsServer(port) 113 | }else{ 114 | startWsServer(port); 115 | } 116 | } 117 | 118 | 119 | function stopWsServer(){ 120 | return new Promise((resolve) => { 121 | if(isRunning){ 122 | Reporter.webSocketReporter = null; 123 | server.close(() => { 124 | Log.i(`WebSocket Server stopped.`); 125 | isRunning = false; 126 | resolve(); 127 | }) 128 | }else{ 129 | resolve(); 130 | } 131 | }) 132 | } 133 | 134 | 135 | init(); 136 | 137 | module.exports = { 138 | getStatus: () => { 139 | return { 140 | status: isRunning, 141 | msg: errorMsg 142 | } 143 | }, 144 | 145 | startWsServer, 146 | restartWsServer, 147 | stopWsServer 148 | } -------------------------------------------------------------------------------- /src/lib/validation.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { isUtf8 } = require('buffer'); 4 | 5 | // 6 | // Allowed token characters: 7 | // 8 | // '!', '#', '$', '%', '&', ''', '*', '+', '-', 9 | // '.', 0-9, A-Z, '^', '_', '`', a-z, '|', '~' 10 | // 11 | // tokenChars[32] === 0 // ' ' 12 | // tokenChars[33] === 1 // '!' 13 | // tokenChars[34] === 0 // '"' 14 | // ... 15 | // 16 | // prettier-ignore 17 | const tokenChars = [ 18 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0 - 15 19 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31 20 | 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, // 32 - 47 21 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48 - 63 22 | 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64 - 79 23 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, // 80 - 95 24 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96 - 111 25 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0 // 112 - 127 26 | ]; 27 | 28 | /** 29 | * Checks if a status code is allowed in a close frame. 30 | * 31 | * @param {Number} code The status code 32 | * @return {Boolean} `true` if the status code is valid, else `false` 33 | * @public 34 | */ 35 | function isValidStatusCode(code) { 36 | return ( 37 | (code >= 1000 && 38 | code <= 1014 && 39 | code !== 1004 && 40 | code !== 1005 && 41 | code !== 1006) || 42 | (code >= 3000 && code <= 4999) 43 | ); 44 | } 45 | 46 | /** 47 | * Checks if a given buffer contains only correct UTF-8. 48 | * Ported from https://www.cl.cam.ac.uk/%7Emgk25/ucs/utf8_check.c by 49 | * Markus Kuhn. 50 | * 51 | * @param {Buffer} buf The buffer to check 52 | * @return {Boolean} `true` if `buf` contains only correct UTF-8, else `false` 53 | * @public 54 | */ 55 | function _isValidUTF8(buf) { 56 | const len = buf.length; 57 | let i = 0; 58 | 59 | while (i < len) { 60 | if ((buf[i] & 0x80) === 0) { 61 | // 0xxxxxxx 62 | i++; 63 | } else if ((buf[i] & 0xe0) === 0xc0) { 64 | // 110xxxxx 10xxxxxx 65 | if ( 66 | i + 1 === len || 67 | (buf[i + 1] & 0xc0) !== 0x80 || 68 | (buf[i] & 0xfe) === 0xc0 // Overlong 69 | ) { 70 | return false; 71 | } 72 | 73 | i += 2; 74 | } else if ((buf[i] & 0xf0) === 0xe0) { 75 | // 1110xxxx 10xxxxxx 10xxxxxx 76 | if ( 77 | i + 2 >= len || 78 | (buf[i + 1] & 0xc0) !== 0x80 || 79 | (buf[i + 2] & 0xc0) !== 0x80 || 80 | (buf[i] === 0xe0 && (buf[i + 1] & 0xe0) === 0x80) || // Overlong 81 | (buf[i] === 0xed && (buf[i + 1] & 0xe0) === 0xa0) // Surrogate (U+D800 - U+DFFF) 82 | ) { 83 | return false; 84 | } 85 | 86 | i += 3; 87 | } else if ((buf[i] & 0xf8) === 0xf0) { 88 | // 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 89 | if ( 90 | i + 3 >= len || 91 | (buf[i + 1] & 0xc0) !== 0x80 || 92 | (buf[i + 2] & 0xc0) !== 0x80 || 93 | (buf[i + 3] & 0xc0) !== 0x80 || 94 | (buf[i] === 0xf0 && (buf[i + 1] & 0xf0) === 0x80) || // Overlong 95 | (buf[i] === 0xf4 && buf[i + 1] > 0x8f) || 96 | buf[i] > 0xf4 // > U+10FFFF 97 | ) { 98 | return false; 99 | } 100 | 101 | i += 4; 102 | } else { 103 | return false; 104 | } 105 | } 106 | 107 | return true; 108 | } 109 | 110 | module.exports = { 111 | isValidStatusCode, 112 | isValidUTF8: _isValidUTF8, 113 | tokenChars 114 | }; 115 | 116 | if (isUtf8) { 117 | module.exports.isValidUTF8 = function (buf) { 118 | return buf.length < 24 ? _isValidUTF8(buf) : isUtf8(buf); 119 | }; 120 | } 121 | // /* istanbul ignore else */ else if (!process.env.WS_NO_UTF_8_VALIDATE) { 122 | // try { 123 | // const isValidUTF8 = require('utf-8-validate'); 124 | // 125 | // module.exports.isValidUTF8 = function (buf) { 126 | // return buf.length < 32 ? _isValidUTF8(buf) : isValidUTF8(buf); 127 | // }; 128 | // } catch (e) { 129 | // // Continue regardless of the error. 130 | // } 131 | // } 132 | -------------------------------------------------------------------------------- /doc/message.md: -------------------------------------------------------------------------------- 1 | # 聊天消息 2 | 3 | ### 支持的消息类型 4 | 5 | | 消息类型 | 接收 | 发送 | 备注 | 6 | |---------|:--:|:--:|-----------------------| 7 | | 文字 | ✔ | ✔ | | 8 | | QQ表情 | ✔ | ✔ | | 9 | | 图片 | ✔ | ✔ | 发送目前仅支持本地图片 | 10 | | 语音 | | 11 | | 短视频 | | 12 | | at | ✔ | ✔ | | 13 | | 猜拳魔法表情 | | 14 | | 掷骰子魔法表情 | | 15 | | 窗口抖动 | | 16 | | 戳一戳 | ❌ | ❌ | QQNT并没有戳一戳功能,所以该功能不支持 | 17 | | 链接分享 | | 18 | | 推荐好友 | | 19 | | 推荐群 | | 20 | | 回复 | ✔ | ✔ | 只能回复Bot框架启动后收到的消息 | 21 | | 合并转发 | ✔ | 22 | | JSON 消息 | ✔ | ✔ | | 23 | | XML 消息 | | 24 | 25 | ### 消息上报结构 26 | #### 好友消息 27 | ```json lines 28 | { 29 | "time": 1711698307, 30 | "message_id": "7351688249283856975", 31 | "self_id": "123456", 32 | "post_type": "message", 33 | "user_id": "123456", 34 | "font": 0, 35 | "message": [ 36 | { 37 | "type": "text", 38 | "data": { 39 | "text": "hello" 40 | } 41 | } 42 | ], 43 | "raw_message": "hello", 44 | "message_type": "private", 45 | "sub_type": "friend", 46 | "sender": { 47 | "user_id": "123456", 48 | "nickname": "", 49 | "sex": "unknown", 50 | "age": 0 51 | } 52 | } 53 | ``` 54 | 55 | #### 群消息 56 | ```json lines 57 | { 58 | "time": 1711698530, 59 | "message_id": "7351689208262640341", 60 | "self_id": "123456", 61 | "post_type": "message", 62 | "user_id": "123456", 63 | "font": 0, 64 | "message": [ 65 | { 66 | "type": "image", 67 | "data": { 68 | "file": "file:///C:\\xxxx\\123456.jpg", 69 | "url": "https://xxxxxxx", 70 | "md5": "6BC2CAB569525B992398376BA7B3D8D4" 71 | } 72 | } 73 | ], 74 | "raw_message": "[CQ:image,md5=6BC2CAB569525B992398376BA7B3D8D4]", 75 | "group_id": "123456", 76 | "message_type": "group", 77 | "sub_type": "group", 78 | "sender": { 79 | "user_id": "123456", 80 | "nickname": "", 81 | "card": "", 82 | "sex": "unknown", 83 | "age": 0, 84 | "area": "", 85 | "level": "0", 86 | "role": "", 87 | "title": "" 88 | } 89 | } 90 | ``` 91 | #### 频道消息 92 | **注意:只有QQ打开某一具体频道的聊天界面才能接受到频道消息** 93 | ```json lines 94 | { 95 | "time": 1711698669, 96 | "message_id": "7351689810098904374", 97 | "self_id": "123456", 98 | "post_type": "message", 99 | "font": 0, 100 | "message": [ 101 | { 102 | "type": "text", 103 | "data": { 104 | "text": "hello" 105 | } 106 | } 107 | ], 108 | "raw_message": "hello", 109 | "guild_id": "123456", 110 | "channel_id": "123456", 111 | "tiny_id": "123456", 112 | "message_type": "guild", 113 | "sub_type": "message" 114 | } 115 | ``` 116 | 117 | 118 | ## 消息结构 119 | 120 | ### 纯文本 121 | ```json lines 122 | { 123 | "type": "text", 124 | "data": { 125 | "text": "纯文本内容" 126 | } 127 | } 128 | ``` 129 | 130 | ### QQ 表情 131 | ```json lines 132 | { 133 | "type": "face", 134 | "data": { 135 | "id": "123" // string int 均可 136 | } 137 | } 138 | ``` 139 | 140 | ### 图片 141 | ```json lines 142 | { 143 | "type": "image", 144 | "data": { 145 | "file": "path", // 图片文件本地路径 146 | "url": "https://xxx", // 图片URL 147 | "md5": "3F7D797BE1AF0A" // 图片md5 (大写) 148 | } 149 | } 150 | ``` 151 | 152 | ### at 153 | ```json lines 154 | { 155 | "type": "at", 156 | "data": { 157 | "qq": "123456" // at全体成员时是 "all" 158 | } 159 | } 160 | ``` 161 | 162 | ### 回复 163 | ```json lines 164 | { 165 | "type": "reply", 166 | "data": { 167 | "id": "123456" // 回复的消息的msgId 168 | } 169 | } 170 | ``` 171 | 172 | ### 转发消息 173 | ```json lines 174 | { 175 | "type": "forward", 176 | "data": { 177 | "id": "123456" // 消息的msgId 178 | } 179 | } 180 | ``` 181 | 182 | ### Json消息 183 | ```json lines 184 | { 185 | "type": "json", 186 | "data": { 187 | "data": "{\"app\":\"com.tencent.miniapp_01\" ... }" // Json消息内容(字符串) 188 | } 189 | } 190 | ``` -------------------------------------------------------------------------------- /src/logger.js: -------------------------------------------------------------------------------- 1 | // const fs = require('fs'); 2 | // const path = require("path"); 3 | // const WebSocket = require('./lib/websocket'); 4 | 5 | class Log { 6 | 7 | static isDebug = false; 8 | static isDebugIPC = false; 9 | 10 | static logPath = './'; 11 | 12 | static fileStream = null; 13 | static ws = null; 14 | 15 | /** @type IPCDebugger */ 16 | static ipcDebugger = null; 17 | 18 | static setDebug(debug, isDebugIPC, logPath = null){ 19 | this.isDebug = debug; 20 | this.isDebugIPC = isDebugIPC; 21 | if(logPath != null) this.logPath = logPath; 22 | 23 | if(this.isDebugIPC) this.ipcDebugger = new IPCDebugger(); 24 | 25 | if(this.fileStream) this.fileStream.end(); 26 | 27 | if(debug){ 28 | // try{ 29 | // this.ws = new WebSocket("ws://127.0.0.1:12345", { headers: { 'X-Self-ID': 0, 'X-Client-Role': "" }}); 30 | // this.ws.on('open', () => { this.isOpen = true }); 31 | // this.ws.on('close', (code) => { this.isOpen = false }); 32 | // }catch(e){ } 33 | 34 | // let d = new Date(); 35 | // let logFile = path.join(this.logPath, `${d.getFullYear()}-${d.getMonth() + 1}-${d.getDate()} ${d.getHours()}-${d.getMinutes()}-${d.getSeconds()}.log`); 36 | // this.fileStream = null 37 | // this.fileStream = fs.createWriteStream(logFile); 38 | // Log.i(`debug mode is on, debug log to ${logFile}`) 39 | } 40 | } 41 | 42 | static d(...args){ 43 | if(this.isDebug){ 44 | console.log("\x1b[36m[OneBotAPI-Debug]\x1b[0m", ...args); 45 | const data = args.join('') 46 | if(this.ws) this.ws.send("\x1b[36m[Debug]\x1b[0m" + data + '\n'); 47 | this.fileStream?.write("[Debug]" + data + '\n'); 48 | } 49 | } 50 | 51 | static i(...args){ 52 | console.log("\x1b[32m[OneBotAPI-Info]\x1b[0m", ...args); 53 | if(this.isDebug){ 54 | const data = args.join('') 55 | if(this.ws) this.ws.send("\x1b[32m[Info]\x1b[0m" + data + '\n'); 56 | this.fileStream?.write("[Info]" + data + '\n'); 57 | } 58 | } 59 | 60 | static w(...args){ 61 | console.log("\x1b[33m[OneBotAPI-Warn]\x1b[0m", ...args); 62 | if(this.isDebug){ 63 | const data = args.join('') 64 | if(this.ws) this.ws.send("\x1b[33m[Warn]\x1b[0m" + data + '\n'); 65 | this.fileStream?.write("[Warn]" + data + '\n'); 66 | } 67 | } 68 | 69 | static e(...args){ 70 | console.log("\x1b[31m[OneBotAPI-Error]\x1b[0m", ...args); 71 | if(this.isDebug){ 72 | const data = args.join('') 73 | if(this.ws) this.ws.send("\x1b[31m[Error]\x1b[0m" + data + '\n'); 74 | this.fileStream?.write("[Error]" + data + '\n'); 75 | } 76 | } 77 | } 78 | 79 | 80 | class IPCDebugger { 81 | 82 | debugIPC = {}; 83 | 84 | blackList = [ 85 | "nodeIKernelMsgListener/onAddSendMsg", 86 | "nodeIKernelMsgListener/onMsgInfoListUpdate", 87 | // "nodeIKernelProfileListener/onProfileDetailInfoChanged", 88 | "nodeIKernelRecentContactListener/onRecentContactListChangedVer2", 89 | ] 90 | 91 | constructor(){ 92 | 93 | } 94 | 95 | IPCSend(_, status, name, ...args) { 96 | if(name === '___!log') return; 97 | let eventName = args?.[0]?.[0]?.eventName; 98 | if(eventName?.startsWith("ns-LoggerApi")) return; 99 | let callbackId = args?.[0]?.[0]?.callbackId; 100 | if(callbackId){ 101 | // if(str.length > 100) str = str.slice(0, 45) + ' ... ' + str.slice(str.length - 45); 102 | this.debugIPC[callbackId] = args[0]; 103 | // Log.d(`[IPC Call ${callbackId}] -> ${JSON.stringify(args)}`); 104 | }else{ 105 | Log.d(`[IPC Call] -> ${JSON.stringify(args)}`); 106 | } 107 | } 108 | 109 | IPCReceive(channel, ...args){ 110 | if(args?.[0]?.eventName?.startsWith("ns-LoggerApi")) return; 111 | 112 | let callbackId = args?.[0]?.callbackId; 113 | if(callbackId){ 114 | if(this.debugIPC[callbackId]){ 115 | if(args?.[1]){ 116 | let str = JSON.stringify(args?.[1]); 117 | // if(str.length > 100) str = str.slice(0, 45) + ' ... ' + str.slice(str.length - 45); 118 | let ipc = this.debugIPC[callbackId] 119 | Log.d(`\x1b[36m[IPC Func]\x1b[0m ${ipc[1][0]}: ${JSON.stringify(ipc[1][1])} \x1b[36m=>\x1b[0m ${str}`); 120 | } 121 | delete this.debugIPC[callbackId]; 122 | }else{ 123 | if(args?.[1]){ 124 | let str = JSON.stringify(args?.[1]); 125 | Log.d(`[IPC Resp ${callbackId}] <- ${str}`); 126 | } 127 | } 128 | }else{ 129 | let cmdName = args?.[1]?.[0]?.cmdName; 130 | if(cmdName){ 131 | if(cmdName.includes("onBuddyListChange")) return; 132 | else if(cmdName in this.blackList) return; 133 | Log.d(`[IPC Resp] <- ${cmdName}: ${JSON.stringify(args[1][0]?.["payload"])}`); 134 | } 135 | } 136 | 137 | } 138 | } 139 | 140 | module.exports = { 141 | Log, 142 | } -------------------------------------------------------------------------------- /src/lib/stream.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Duplex } = require('stream'); 4 | 5 | /** 6 | * Emits the `'close'` event on a stream. 7 | * 8 | * @param {Duplex} stream The stream. 9 | * @private 10 | */ 11 | function emitClose(stream) { 12 | stream.emit('close'); 13 | } 14 | 15 | /** 16 | * The listener of the `'end'` event. 17 | * 18 | * @private 19 | */ 20 | function duplexOnEnd() { 21 | if (!this.destroyed && this._writableState.finished) { 22 | this.destroy(); 23 | } 24 | } 25 | 26 | /** 27 | * The listener of the `'error'` event. 28 | * 29 | * @param {Error} err The error 30 | * @private 31 | */ 32 | function duplexOnError(err) { 33 | this.removeListener('error', duplexOnError); 34 | this.destroy(); 35 | if (this.listenerCount('error') === 0) { 36 | // Do not suppress the throwing behavior. 37 | this.emit('error', err); 38 | } 39 | } 40 | 41 | /** 42 | * Wraps a `WebSocket` in a duplex stream. 43 | * 44 | * @param {WebSocket} ws The `WebSocket` to wrap 45 | * @param {Object} [options] The options for the `Duplex` constructor 46 | * @return {Duplex} The duplex stream 47 | * @public 48 | */ 49 | function createWebSocketStream(ws, options) { 50 | let terminateOnDestroy = true; 51 | 52 | const duplex = new Duplex({ 53 | ...options, 54 | autoDestroy: false, 55 | emitClose: false, 56 | objectMode: false, 57 | writableObjectMode: false 58 | }); 59 | 60 | ws.on('message', function message(msg, isBinary) { 61 | const data = 62 | !isBinary && duplex._readableState.objectMode ? msg.toString() : msg; 63 | 64 | if (!duplex.push(data)) ws.pause(); 65 | }); 66 | 67 | ws.once('error', function error(err) { 68 | if (duplex.destroyed) return; 69 | 70 | // Prevent `ws.terminate()` from being called by `duplex._destroy()`. 71 | // 72 | // - If the `'error'` event is emitted before the `'open'` event, then 73 | // `ws.terminate()` is a noop as no socket is assigned. 74 | // - Otherwise, the error is re-emitted by the listener of the `'error'` 75 | // event of the `Receiver` object. The listener already closes the 76 | // connection by calling `ws.close()`. This allows a close frame to be 77 | // sent to the other peer. If `ws.terminate()` is called right after this, 78 | // then the close frame might not be sent. 79 | terminateOnDestroy = false; 80 | duplex.destroy(err); 81 | }); 82 | 83 | ws.once('close', function close() { 84 | if (duplex.destroyed) return; 85 | 86 | duplex.push(null); 87 | }); 88 | 89 | duplex._destroy = function (err, callback) { 90 | if (ws.readyState === ws.CLOSED) { 91 | callback(err); 92 | process.nextTick(emitClose, duplex); 93 | return; 94 | } 95 | 96 | let called = false; 97 | 98 | ws.once('error', function error(err) { 99 | called = true; 100 | callback(err); 101 | }); 102 | 103 | ws.once('close', function close() { 104 | if (!called) callback(err); 105 | process.nextTick(emitClose, duplex); 106 | }); 107 | 108 | if (terminateOnDestroy) ws.terminate(); 109 | }; 110 | 111 | duplex._final = function (callback) { 112 | if (ws.readyState === ws.CONNECTING) { 113 | ws.once('open', function open() { 114 | duplex._final(callback); 115 | }); 116 | return; 117 | } 118 | 119 | // If the value of the `_socket` property is `null` it means that `ws` is a 120 | // client websocket and the handshake failed. In fact, when this happens, a 121 | // socket is never assigned to the websocket. Wait for the `'error'` event 122 | // that will be emitted by the websocket. 123 | if (ws._socket === null) return; 124 | 125 | if (ws._socket._writableState.finished) { 126 | callback(); 127 | if (duplex._readableState.endEmitted) duplex.destroy(); 128 | } else { 129 | ws._socket.once('finish', function finish() { 130 | // `duplex` is not destroyed here because the `'end'` event will be 131 | // emitted on `duplex` after this `'finish'` event. The EOF signaling 132 | // `null` chunk is, in fact, pushed when the websocket emits `'close'`. 133 | callback(); 134 | }); 135 | ws.close(); 136 | } 137 | }; 138 | 139 | duplex._read = function () { 140 | if (ws.isPaused) ws.resume(); 141 | }; 142 | 143 | duplex._write = function (chunk, encoding, callback) { 144 | if (ws.readyState === ws.CONNECTING) { 145 | ws.once('open', function open() { 146 | duplex._write(chunk, encoding, callback); 147 | }); 148 | return; 149 | } 150 | 151 | ws.send(chunk, callback); 152 | }; 153 | 154 | duplex.on('end', duplexOnEnd); 155 | duplex.on('error', duplexOnError); 156 | return duplex; 157 | } 158 | 159 | module.exports = createWebSocketStream; 160 | -------------------------------------------------------------------------------- /src/oneBot11/event.js: -------------------------------------------------------------------------------- 1 | const { Log } = require("../logger"); 2 | const { Data } = require("../main/core"); 3 | const { parseFromQQNT } = require("./message"); 4 | 5 | class Event{ 6 | eventType = 0; 7 | constructor(time = 0, message_id = "", post_type = ""){ 8 | this.time = time; 9 | this.message_id = message_id; 10 | this.self_id = Data.selfInfo.uin; 11 | this.post_type = post_type; 12 | } 13 | } 14 | 15 | class MessageEvent extends Event{ 16 | eventType = 0; 17 | constructor(time = 0, message_id = "", user_id = 0, message = []){ 18 | super(time, message_id, "message"); 19 | this.user_id = user_id; 20 | this.font = 0; 21 | /** @type [Message] */ 22 | this.message = message; 23 | this.raw_message = this.message.map(item => item.toCqCode()).join(''); 24 | } 25 | } 26 | 27 | class FriendMessage extends MessageEvent{ 28 | eventType = 1; 29 | constructor(QQNTMsg, user_id = 0, message = []){ 30 | super(parseInt(QQNTMsg?.msgTime || 0), QQNTMsg.msgId, user_id, message); 31 | this.message_type = "private"; 32 | this.sub_type = "friend" 33 | this.sender = { 34 | user_id: user_id, 35 | nickname: "", 36 | sex: "unknown", // 性别,male 或 female 或 37 | age: 0 38 | } 39 | } 40 | } 41 | 42 | 43 | class GroupMessage extends MessageEvent{ 44 | eventType = 2; 45 | constructor(QQNTMsg, user_id = 0, message = []){ 46 | super(parseInt(QQNTMsg?.msgTime || 0), QQNTMsg.msgId, user_id, message); 47 | this.group_id = QQNTMsg.peerUid; 48 | this.message_type = "group"; 49 | this.sub_type = "group" 50 | this.sender = { 51 | user_id: user_id, 52 | nickname: "", 53 | card: "", 54 | sex: "unknown", // 性别,male 或 female 或 55 | age: 0, 56 | area: "", 57 | level: "0", 58 | role: "", 59 | title: "" 60 | } 61 | } 62 | } 63 | 64 | class GuildMessage extends MessageEvent{ 65 | eventType = 4; 66 | constructor(QQNTMsg, user_id = 0, message = []){ 67 | super(parseInt(QQNTMsg?.msgTime || 0), QQNTMsg.msgId, 0, message); 68 | this.guild_id = QQNTMsg.guildId; 69 | this.channel_id = QQNTMsg.channelId; 70 | this.tiny_id = user_id; 71 | this.message_type = "guild"; 72 | this.sub_type = "message" 73 | } 74 | } 75 | 76 | class GrayMessage extends MessageEvent{ 77 | eventType = 0; 78 | constructor(QQNTMsg, user_id = 0, message = []){ 79 | super(parseInt(QQNTMsg?.msgTime || 0), QQNTMsg.msgId, user_id, message); 80 | this.eventType = QQNTMsg.chatType; 81 | if(this.eventType == 2) this.group_id = QQNTMsg.peerUid; 82 | this.raw_message = JSON.stringify(QQNTMsg.elements[0].grayTipElement, (key, value) => { 83 | if( value == null) return undefined; else return value; 84 | }); 85 | } 86 | } 87 | 88 | class RecallMessage extends Event{ 89 | constructor(time, message_id, notice_type, user_id, operator_id, group_id = -1){ 90 | super(time, message_id, "notice"); 91 | this.notice_type = notice_type; 92 | 93 | this.user_id = user_id; 94 | this.operator_id = operator_id; 95 | if(group_id != -1) this.group_id = group_id; 96 | } 97 | 98 | static async parseFromQQNT(QQNTMsg){ 99 | let time = parseInt(QQNTMsg?.msgTime || 0) 100 | if(QQNTMsg.chatType === 1){ 101 | let user_id = Data.getInfoByUid(QQNTMsg.senderUid)?.uin; 102 | if(!user_id){ 103 | Log.w(`无法获取发送者QQ号: uid: ${QQNTMsg.senderUid}`); 104 | user_id = ""; 105 | } 106 | return new RecallMessage(time, QQNTMsg.message_id, "friend_recall", user_id, user_id); 107 | }else if(QQNTMsg.chatType === 2){ 108 | let group_id = QQNTMsg.peerUid; 109 | let user_id = Data.userMap[QQNTMsg.senderUid] || (await Data.getGroupMemberByUid(QQNTMsg.peerUid, QQNTMsg.senderUid))?.uin; 110 | let operator_id = user_id; 111 | if(user_id){ 112 | for(let element of QQNTMsg.elements){ 113 | if(element.elementType == 8 && element.grayTipElement.subElementType == 1){ 114 | let operatorUid = element.grayTipElement.revokeElement.operatorUid; 115 | operator_id = Data.userMap[operatorUid] || (await Data.getGroupMemberByUid(QQNTMsg.group_id, operatorUid))?.uin; 116 | break; 117 | } 118 | } 119 | }else{ 120 | Log.w(`无法获取发送者QQ号: uid: ${QQNTMsg.senderUid}`); 121 | user_id = ""; 122 | operator_id = ""; 123 | } 124 | return new RecallMessage(time, QQNTMsg.message_id, "group_recall", user_id, operator_id, group_id); 125 | } 126 | } 127 | } 128 | 129 | /** 130 | * 将收到的QQNTMessage转换为OneBot11Message 131 | * @param QQNTMsg { QQNTMessage } 132 | * @return { MessageEvent | null } 133 | */ 134 | async function parseMessage(QQNTMsg){ 135 | if(QQNTMsg.msgType == 5){ 136 | return new GrayMessage(QQNTMsg) 137 | } 138 | 139 | let user_id = null; 140 | 141 | if(QQNTMsg.chatType === 1){ 142 | user_id = Data.getInfoByUid(QQNTMsg.senderUid)?.uin; 143 | }else if(QQNTMsg.chatType === 2){ 144 | user_id = Data.userMap[QQNTMsg.senderUid] || (await Data.getGroupMemberByUid(QQNTMsg.peerUid, QQNTMsg.senderUid))?.uin; 145 | }else if(QQNTMsg.chatType === 4){ 146 | user_id = QQNTMsg.senderUid 147 | } 148 | 149 | if(user_id){ 150 | let message = []; 151 | for(let element of QQNTMsg.elements) message.push(await parseFromQQNT(QQNTMsg, element)); 152 | 153 | if(QQNTMsg.chatType === 1){ 154 | return new FriendMessage(QQNTMsg, user_id, message) 155 | }else if(QQNTMsg.chatType === 2){ 156 | return new GroupMessage(QQNTMsg, user_id, message) 157 | }else if(QQNTMsg.chatType === 4){ 158 | return new GuildMessage(QQNTMsg, user_id, message) 159 | }else{ 160 | return null; 161 | } 162 | }else{ 163 | Log.w(`解析新消息失败,无法获取发送者QQ号: uid: ${QQNTMsg.senderUid}`); 164 | return null; 165 | } 166 | } 167 | 168 | module.exports = { 169 | MessageEvent, 170 | FriendMessage, 171 | GroupMessage, 172 | RecallMessage, 173 | parseMessage 174 | } -------------------------------------------------------------------------------- /src/lib/extension.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { tokenChars } = require('./validation'); 4 | 5 | /** 6 | * Adds an offer to the map of extension offers or a parameter to the map of 7 | * parameters. 8 | * 9 | * @param {Object} dest The map of extension offers or parameters 10 | * @param {String} name The extension or parameter name 11 | * @param {(Object|Boolean|String)} elem The extension parameters or the 12 | * parameter value 13 | * @private 14 | */ 15 | function push(dest, name, elem) { 16 | if (dest[name] === undefined) dest[name] = [elem]; 17 | else dest[name].push(elem); 18 | } 19 | 20 | /** 21 | * Parses the `Sec-WebSocket-Extensions` header into an object. 22 | * 23 | * @param {String} header The field value of the header 24 | * @return {Object} The parsed object 25 | * @public 26 | */ 27 | function parse(header) { 28 | const offers = Object.create(null); 29 | let params = Object.create(null); 30 | let mustUnescape = false; 31 | let isEscaping = false; 32 | let inQuotes = false; 33 | let extensionName; 34 | let paramName; 35 | let start = -1; 36 | let code = -1; 37 | let end = -1; 38 | let i = 0; 39 | 40 | for (; i < header.length; i++) { 41 | code = header.charCodeAt(i); 42 | 43 | if (extensionName === undefined) { 44 | if (end === -1 && tokenChars[code] === 1) { 45 | if (start === -1) start = i; 46 | } else if ( 47 | i !== 0 && 48 | (code === 0x20 /* ' ' */ || code === 0x09) /* '\t' */ 49 | ) { 50 | if (end === -1 && start !== -1) end = i; 51 | } else if (code === 0x3b /* ';' */ || code === 0x2c /* ',' */) { 52 | if (start === -1) { 53 | throw new SyntaxError(`Unexpected character at index ${i}`); 54 | } 55 | 56 | if (end === -1) end = i; 57 | const name = header.slice(start, end); 58 | if (code === 0x2c) { 59 | push(offers, name, params); 60 | params = Object.create(null); 61 | } else { 62 | extensionName = name; 63 | } 64 | 65 | start = end = -1; 66 | } else { 67 | throw new SyntaxError(`Unexpected character at index ${i}`); 68 | } 69 | } else if (paramName === undefined) { 70 | if (end === -1 && tokenChars[code] === 1) { 71 | if (start === -1) start = i; 72 | } else if (code === 0x20 || code === 0x09) { 73 | if (end === -1 && start !== -1) end = i; 74 | } else if (code === 0x3b || code === 0x2c) { 75 | if (start === -1) { 76 | throw new SyntaxError(`Unexpected character at index ${i}`); 77 | } 78 | 79 | if (end === -1) end = i; 80 | push(params, header.slice(start, end), true); 81 | if (code === 0x2c) { 82 | push(offers, extensionName, params); 83 | params = Object.create(null); 84 | extensionName = undefined; 85 | } 86 | 87 | start = end = -1; 88 | } else if (code === 0x3d /* '=' */ && start !== -1 && end === -1) { 89 | paramName = header.slice(start, i); 90 | start = end = -1; 91 | } else { 92 | throw new SyntaxError(`Unexpected character at index ${i}`); 93 | } 94 | } else { 95 | // 96 | // The value of a quoted-string after unescaping must conform to the 97 | // token ABNF, so only token characters are valid. 98 | // Ref: https://tools.ietf.org/html/rfc6455#section-9.1 99 | // 100 | if (isEscaping) { 101 | if (tokenChars[code] !== 1) { 102 | throw new SyntaxError(`Unexpected character at index ${i}`); 103 | } 104 | if (start === -1) start = i; 105 | else if (!mustUnescape) mustUnescape = true; 106 | isEscaping = false; 107 | } else if (inQuotes) { 108 | if (tokenChars[code] === 1) { 109 | if (start === -1) start = i; 110 | } else if (code === 0x22 /* '"' */ && start !== -1) { 111 | inQuotes = false; 112 | end = i; 113 | } else if (code === 0x5c /* '\' */) { 114 | isEscaping = true; 115 | } else { 116 | throw new SyntaxError(`Unexpected character at index ${i}`); 117 | } 118 | } else if (code === 0x22 && header.charCodeAt(i - 1) === 0x3d) { 119 | inQuotes = true; 120 | } else if (end === -1 && tokenChars[code] === 1) { 121 | if (start === -1) start = i; 122 | } else if (start !== -1 && (code === 0x20 || code === 0x09)) { 123 | if (end === -1) end = i; 124 | } else if (code === 0x3b || code === 0x2c) { 125 | if (start === -1) { 126 | throw new SyntaxError(`Unexpected character at index ${i}`); 127 | } 128 | 129 | if (end === -1) end = i; 130 | let value = header.slice(start, end); 131 | if (mustUnescape) { 132 | value = value.replace(/\\/g, ''); 133 | mustUnescape = false; 134 | } 135 | push(params, paramName, value); 136 | if (code === 0x2c) { 137 | push(offers, extensionName, params); 138 | params = Object.create(null); 139 | extensionName = undefined; 140 | } 141 | 142 | paramName = undefined; 143 | start = end = -1; 144 | } else { 145 | throw new SyntaxError(`Unexpected character at index ${i}`); 146 | } 147 | } 148 | } 149 | 150 | if (start === -1 || inQuotes || code === 0x20 || code === 0x09) { 151 | throw new SyntaxError('Unexpected end of input'); 152 | } 153 | 154 | if (end === -1) end = i; 155 | const token = header.slice(start, end); 156 | if (extensionName === undefined) { 157 | push(offers, token, params); 158 | } else { 159 | if (paramName === undefined) { 160 | push(params, token, true); 161 | } else if (mustUnescape) { 162 | push(params, paramName, token.replace(/\\/g, '')); 163 | } else { 164 | push(params, paramName, token); 165 | } 166 | push(offers, extensionName, params); 167 | } 168 | 169 | return offers; 170 | } 171 | 172 | /** 173 | * Builds the `Sec-WebSocket-Extensions` header field value. 174 | * 175 | * @param {Object} extensions The map of extensions and parameters to format 176 | * @return {String} A string representing the given object 177 | * @public 178 | */ 179 | function format(extensions) { 180 | return Object.keys(extensions) 181 | .map((extension) => { 182 | let configurations = extensions[extension]; 183 | if (!Array.isArray(configurations)) configurations = [configurations]; 184 | return configurations 185 | .map((params) => { 186 | return [extension] 187 | .concat( 188 | Object.keys(params).map((k) => { 189 | let values = params[k]; 190 | if (!Array.isArray(values)) values = [values]; 191 | return values 192 | .map((v) => (v === true ? k : `${k}=${v}`)) 193 | .join('; '); 194 | }) 195 | ) 196 | .join('; '); 197 | }) 198 | .join(', '); 199 | }) 200 | .join(', '); 201 | } 202 | 203 | module.exports = { format, parse }; 204 | -------------------------------------------------------------------------------- /src/main/main.js: -------------------------------------------------------------------------------- 1 | const { ipcMain, BrowserWindow } = require("electron"); 2 | 3 | const { IPCAction, defaultSetting} = require('../common/const'); 4 | const { Data, Setting, Reporter} = require('./core'); 5 | 6 | const { Log } = require('../logger'); 7 | const utils = require('../utils'); 8 | 9 | const wsServer = require('../network/wsServer'); 10 | const wsReverse = require('../network/wsReverseServer'); 11 | const httpServer = require('../network/httpServer'); 12 | const httpReporter = require("../network/httpReporter"); 13 | 14 | const { QQNtAPI} = require('../qqnt/QQNtAPI'); 15 | const IPCHandle = require('../qqnt/IpcHandle'); 16 | const {oneBot11API} = require("../oneBot11/oneBot11"); 17 | 18 | 19 | let isLoaded = false; 20 | 21 | function onLoad(plugin) { 22 | 23 | const setting = utils.loadSetting(plugin); 24 | if(setting){ 25 | Setting.setting = utils.checkAndCompleteKeys(setting, defaultSetting); 26 | }else{ 27 | utils.saveSetting(defaultSetting); 28 | } 29 | 30 | Log.setDebug(Setting.setting.debug.debug, Setting.setting.debug.ipc, plugin.path.data); 31 | 32 | ipcMain.on('one_bot_api_get_runtime_data', (event) => { 33 | event.returnValue = { 34 | 'IPCAction': IPCAction, 35 | 'Setting': Setting.setting, 36 | 'isDebug': Log.isDebug 37 | }; 38 | }); 39 | 40 | // 获取设置 41 | ipcMain.handle(IPCAction.ACTION_GET_CONFIG, (event) => Setting.setting); 42 | 43 | // 获取服务状态 44 | ipcMain.handle(IPCAction.ACTION_SERVER_STATUS, (event) => { 45 | return { 46 | http: httpServer.getStatus(), 47 | ws: wsServer.getStatus(), 48 | wsReverse: wsReverse.getStatus() 49 | } 50 | }); 51 | 52 | ipcMain.handle(IPCAction.ACTION_GET_FRIENDS, () => { 53 | return Object.values(Data.friends); 54 | }); 55 | 56 | ipcMain.handle(IPCAction.ACTION_GET_GROUPS, () => { 57 | return Object.values(Data.groups); 58 | }); 59 | 60 | ipcMain.on(IPCAction.ACTION_LOG, (event, args) => { 61 | console.log("\x1b[32m[OneBotAPI-Render]\x1b[0m", ...args); 62 | }); 63 | 64 | ipcMain.on(IPCAction.ACTION_LOAD_MAIN_PAGE, (event, arg) => { 65 | if(isLoaded){ 66 | Log.w('主页面已加载'); 67 | return false; 68 | } 69 | 70 | const window = BrowserWindow.getAllWindows().find((window) => window.webContents.getURL().includes('#/main/message')); 71 | if(window){ 72 | Log.i("正在加载Bot框架"); 73 | 74 | QQNtAPI.ntCall("ns-GlobalDataApi", "fetchAuthData", []).then(info => { 75 | Log.d(`当前账号信息: uid: ${info.uid}, uin: ${info.uin}`); 76 | Data.selfInfo = info; 77 | }); 78 | 79 | // 获取好友列表 80 | QQNtAPI.ntCall("ns-ntApi", "nodeIKernelBuddyService/getBuddyList", [{ force_update: false }, undefined]).then(); 81 | // 获取群列表 82 | QQNtAPI.ntCall("ns-ntApi", "nodeIKernelGroupService/getGroupList", [{ force_update: false }, undefined]).then(); 83 | 84 | // QQNtAPI.getGuildList().then(data => Data.guilds = data); 85 | 86 | // [{ 87 | // "eventName": "ns-ntApi-5" 88 | // }, ["nodeIKernelMsgService/getAllJoinGuildCnt", null, null]] 89 | // 90 | // { 91 | // "result": 0, "errMsg": "", "number": 2 92 | // } 93 | 94 | if(Setting.setting.http.enableServer) httpServer.startHttpServer(Setting.setting.http.port); 95 | if(Setting.setting.http.enable || Setting.setting.http.enableReport) httpReporter.startHttpReport(); 96 | if(Setting.setting.ws.enable) wsServer.startWsServer(Setting.setting.ws.port); 97 | if(Setting.setting.wsReverse.enable) wsReverse.startWsClient(Setting.setting.wsReverse); 98 | Reporter.isLoaded = true; 99 | 100 | Log.i("Bot框架加载完成"); 101 | isLoaded = true; 102 | return true; 103 | } 104 | 105 | Log.e('无法加载Bot框架'); 106 | return false; 107 | }); 108 | 109 | ipcMain.on(IPCAction.ACTION_SET_CONFIG, (event, setting) => { 110 | Setting.setting = setting; 111 | Log.setDebug(setting.debug.debug, setting.debug.ipc); 112 | if(Setting.setting.http.enableServer){ 113 | Log.i('start http api'); 114 | httpServer.startHttpServer(Setting.setting.http.port); 115 | }else{ 116 | Log.i('stop http api'); 117 | httpServer.stopHttpServer().then() 118 | } 119 | if(Setting.setting.http.enable || Setting.setting.http.enableReport){ 120 | Log.i('start http report'); 121 | httpReporter.startHttpReport() 122 | }else{ 123 | Log.i('stop http report'); 124 | httpReporter.stopHttpReport() 125 | } 126 | utils.saveSetting(setting); 127 | }); 128 | 129 | ipcMain.on(IPCAction.ACTION_RESTART_HTTP_SERVER, (event, port) => httpServer.restartHttpServer(port).then()); 130 | 131 | ipcMain.on(IPCAction.ACTION_RESTART_WS_SERVER, (event, port) => wsServer.restartWsServer(port).then()); 132 | ipcMain.on(IPCAction.ACTION_STOP_WS_SERVER, (event) => wsServer.stopWsServer().then()); 133 | 134 | ipcMain.on(IPCAction.ACTION_RESTART_WS_REVERSE_SERVER, (event, params) => wsReverse.restartWsClient(params)); 135 | ipcMain.on(IPCAction.ACTION_STOP_WS_REVERSE_SERVER, (event) => wsReverse.stopWsClient()); 136 | 137 | ipcMain.handle(IPCAction.ACTION_HTTP_TEST, async (event, url) => { 138 | try{ 139 | return await oneBot11API[url]({}) 140 | } catch(e){ 141 | return { 142 | 'code': -1, 143 | 'msg': e.toString() 144 | } 145 | } 146 | }); 147 | } 148 | 149 | 150 | function onBrowserWindowCreated(window){ 151 | const original_send = (window.webContents.__qqntim_original_object && window.webContents.__qqntim_original_object.send) || window.webContents.send; 152 | const patched_send = function(channel, ...args){ 153 | if(!patchedSend(channel, ...args)){ 154 | // 调用原始的send方法 155 | return original_send.call(window.webContents, channel, ...args); 156 | } 157 | }; 158 | 159 | if(window.webContents.__qqntim_original_object){ 160 | window.webContents.__qqntim_original_object.send = patched_send; 161 | } 162 | window.webContents.send = patched_send; 163 | 164 | const original_ipc_message = window.webContents._events["-ipc-message"]?.[0] || window.webContents._events["-ipc-message"]; 165 | const proxyEvents = new Proxy(original_ipc_message, { 166 | apply(target, thisArg, argumentsList){ 167 | patchedIPC(...argumentsList) 168 | return target.apply(thisArg, argumentsList); 169 | } 170 | }); 171 | 172 | if(window.webContents._events["-ipc-message"][0]){ 173 | window.webContents._events["-ipc-message"][0] = proxyEvents 174 | }else{ 175 | window.webContents._events["-ipc-message"] = proxyEvents 176 | } 177 | } 178 | 179 | 180 | /** 181 | * 监听渲染进程向主进程发送的消息 182 | */ 183 | function patchedIPC(_, status, name, ...args) { 184 | if(Log.isDebugIPC) Log.ipcDebugger.IPCSend(_, status, name, ...args); 185 | } 186 | 187 | /** 188 | * 解析向渲染进程发送的消息 189 | */ 190 | function patchedSend(channel, ...args){ 191 | if(Log.isDebugIPC) Log.ipcDebugger.IPCReceive(channel, ...args); 192 | 193 | const cmdObject = args[1]?.[0]; 194 | const cmdName = cmdObject?.cmdName; 195 | if(cmdName) IPCHandle.onMessageHandle(cmdObject); 196 | 197 | return QQNtAPI.ntCallBack(args, cmdObject) 198 | } 199 | 200 | if(LiteLoader?.plugins?.['OneBotApi-JS']){ 201 | onLoad(LiteLoader.plugins['OneBotApi-JS']); 202 | } 203 | 204 | module.exports = { 205 | onLoad, 206 | onBrowserWindowCreated 207 | } -------------------------------------------------------------------------------- /src/oneBot11/oneBot11.js: -------------------------------------------------------------------------------- 1 | const Api = require("./api"); 2 | const MessageModel = require("./messageModel"); 3 | const { Data } = require("../main/core"); 4 | const { QQNtAPI } = require('../qqnt/QQNtAPI'); 5 | const { createPeer, parseFromQQNT } = require("./message"); 6 | 7 | const oneBot11API = { 8 | 9 | /** 10 | * 获取用户信息 11 | * 12 | */ 13 | '__getUserByUid': async (postData) => { 14 | try{ 15 | return await QQNtAPI.getUserInfoByUid(postData['uid']); 16 | }catch(e){ 17 | return e.stack.toString(); 18 | } 19 | 20 | }, 21 | 22 | '__sendMsg': async (postData) => { 23 | return { 24 | status: 'ok', 25 | retcode: 0, 26 | data: await QQNtAPI.ntCall("ns-ntApi", "nodeIKernelMsgService/sendMsg", [{ 27 | msgId: "0", 28 | peer: postData.peer, 29 | msgElements: postData.elements, 30 | msgAttributeInfos: new Map() 31 | }, null]) 32 | } 33 | }, 34 | 35 | /** 36 | * 发送消息 37 | * { 38 | * "user_id" or "group_id": 123456, 39 | * "message": "test" 40 | * } 41 | * or 42 | * { 43 | * "user_id" or "group_id": 123456, 44 | * "message": [ 45 | * "type": "text", 46 | * "data": [ 47 | * "text": "test" 48 | * ] 49 | * ] 50 | * } 51 | */ 52 | 'send_msg': async (postData) => { 53 | let response = await MessageModel.sendMessage(postData); 54 | if(response?.msg){ 55 | response.status = 'failed'; 56 | response.retcode = 400; 57 | }else{ 58 | response.status = 'ok'; 59 | response.retcode = 0; 60 | } 61 | return response; 62 | }, 63 | 64 | /** 65 | * 发送私聊消息 66 | * { 67 | * "user_id": 123456, 68 | * "message": "test" 69 | * } 70 | * or 71 | * { 72 | * "user_id": 123456, 73 | * "message": [ 74 | * "type": "text", 75 | * "data": [ 76 | * "text": "test" 77 | * ] 78 | * ] 79 | * } 80 | */ 81 | 'send_private_msg': async (postData) => { 82 | let response = await MessageModel.sendMessage(postData); 83 | if(response?.msg){ 84 | response.status = 'failed'; 85 | response.retcode = 400; 86 | }else{ 87 | response.status = 'ok'; 88 | response.retcode = 0; 89 | } 90 | return response; 91 | }, 92 | 93 | /** 94 | * 发送群消息 95 | * { 96 | * "group_id": 123456, 97 | * "message": "test" 98 | * } 99 | * or 100 | * { 101 | * "group_id": 123456, 102 | * "message": [ 103 | * "type": "text", 104 | * "data": [ 105 | * "text": "test" 106 | * ] 107 | * ] 108 | * } 109 | */ 110 | 'send_group_msg': async (postData) => { 111 | let response = await MessageModel.sendMessage(postData); 112 | if(response?.msg){ 113 | response.status = 'failed'; 114 | response.retcode = 400; 115 | }else{ 116 | response.status = 'ok'; 117 | response.retcode = 0; 118 | } 119 | return response; 120 | }, 121 | 122 | /** 123 | * 下载私聊文件 124 | * { 125 | * "user_id": 123456, 126 | * "msgId": e.g. 7310952964011716631, 127 | * "elementId": e.g. 7311516200489877813 128 | * "downloadPath" (可选): "C:/tmp/fileName" 129 | * } 130 | */ 131 | 'download_file': (postData) => { 132 | let userInfo = Data.getInfoByQQ(postData['user_id']); 133 | 134 | if(userInfo == null){ 135 | return { code: 400, msg: `User with QQ ${postData['user_id']} not found.` } 136 | } 137 | 138 | if(!postData["msgId"] || !postData['elementId']){ 139 | return { code: 400, msg: "Must provide 'msgId' and 'elementId'." } 140 | } 141 | 142 | QQNtAPI.ntCall("ns-ntApi", "nodeIKernelMsgService/downloadRichMedia",[ 143 | { 144 | "getReq": { 145 | "msgId": postData['msgId'], 146 | "chatType": 1, 147 | "peerUid": userInfo.uid, 148 | "elementId": postData['elementId'], 149 | "thumbSize": 0, 150 | "downloadType": 1, 151 | "filePath": postData['downloadPath'] || "" 152 | } 153 | } 154 | ]).then(); 155 | 156 | return { status: 'ok', retcode: 0, } 157 | }, 158 | 159 | /** 160 | * 撤回消息 161 | */ 162 | 'delete_msg': async(postData) => { 163 | let peer = null; 164 | let msg = Data.historyMessage.get(postData.message_id); 165 | if(msg){ 166 | peer = { 167 | chatType: msg.chatType, 168 | peerUid: msg.peerUid, 169 | guildId: '' 170 | } 171 | }else{ 172 | let peer = createPeer(postData.group_id, postData.user_id); 173 | if(!peer){ 174 | return { 175 | status: 'failed', 176 | retcode: 400, 177 | msg: "消息不存在" 178 | } 179 | } 180 | } 181 | 182 | let result = await QQNtAPI.ntCall( 183 | "ns-ntApi", 184 | "nodeIKernelMsgService/recallMsg", 185 | [{ peer, "msgIds": [ postData.message_id.toString() ] 186 | }, null]); 187 | 188 | if(result.result == 0){ 189 | return { 190 | status: 'ok', 191 | retcode: 0, 192 | } 193 | }else{ 194 | return { 195 | status: 'failed', 196 | retcode: result.result, 197 | msg: result.errMsg, 198 | } 199 | } 200 | }, 201 | 202 | /** 203 | * 获取消息 204 | */ 205 | 'get_msg': async(postData) => { 206 | if(!postData.message_id){ 207 | return { status: 'failed', "retcode": 400, msg: "Must provide 'message_id'." } 208 | } 209 | 210 | let oneBotMsg = Data.historyMessage.get(postData.message_id)?.oneBotMsg; 211 | if(oneBotMsg){ 212 | return { 213 | status: 'ok', 214 | retcode: 0, 215 | data: { 216 | message: oneBotMsg.message 217 | } 218 | } 219 | }else{ 220 | return { 221 | status: 'failed', 222 | retcode: 404, 223 | msg: `消息不存在, Can't find message with id: ${postData.message_id}`, 224 | } 225 | } 226 | }, 227 | 228 | /** 229 | * 获取合并转发的消息 230 | */ 231 | 'get_forward_msg': async(postData) => { 232 | if(!postData.id){ 233 | return { status: 'failed', "retcode": 400, msg: "Must provide 'id' (msgId)." } 234 | } 235 | 236 | let peer; 237 | let msg = Data.historyMessage.get(postData.id); 238 | if(msg){ 239 | peer = { 240 | chatType: msg.chatType, 241 | peerUid: msg.peerUid, 242 | guildId: '' 243 | } 244 | }else{ 245 | peer = createPeer(postData.group_id, postData.user_id); 246 | if(!peer){ 247 | return { 248 | status: 'failed', 249 | retcode: 400, 250 | msg: "消息不存在" 251 | } 252 | } 253 | } 254 | 255 | let forwardMsg = []; 256 | let messages = await QQNtAPI.getMultiMessages(peer, postData.id); 257 | for(let message of messages){ 258 | let content = []; 259 | for(let element of message.elements){ 260 | content.push(await parseFromQQNT(message, element)); 261 | } 262 | forwardMsg.push({ 263 | "type": "node", 264 | "data": { 265 | "user_id": message.senderUid, 266 | "content": content 267 | } 268 | }) 269 | } 270 | 271 | return { 272 | status: 'ok', 273 | retcode: 0, 274 | data: { 275 | 'message': forwardMsg 276 | } 277 | }; 278 | }, 279 | 280 | /** 281 | * 获取登录号信息 282 | */ 283 | 'get_login_info': () => { 284 | return { 285 | status: 'ok', 286 | retcode: 0, 287 | data: { 288 | 'user_id': Data.selfInfo.account 289 | } 290 | }; 291 | }, 292 | 293 | 294 | /** 295 | * 处理加好友请求 296 | * { 297 | * "flag": 加好友请求的 flag(需从上报的数据中获得) 298 | * "approve": 是否同意请求(true/false) 299 | * } 300 | */ 301 | 'set_friend_add_request': async (postData) => { 302 | if('flag' in postData && 'approve' in postData){ 303 | return { 304 | status: 'ok', 305 | retcode: 0, 306 | data: await QQNtAPI.ntCall( 307 | "ns-ntApi", 308 | "nodeIKernelBuddyService/approvalFriendRequest", 309 | [{ 310 | "approvalInfo":{ 311 | "friendUid": postData["flag"], 312 | "accept": postData['approve'] 313 | } 314 | }, null] 315 | ) 316 | }; 317 | }else{ 318 | return { 319 | status: 'failed', 320 | retcode: 400, 321 | msg: "Must provide 'flag' and 'approve'." 322 | } 323 | } 324 | }, 325 | 326 | // set_group_anonymous_ban 群组匿名用户禁言 327 | // set_group_anonymous 群组匿名 328 | // set_group_card 设置群名片(群备注) 329 | // set_group_special_title 设置群组专属头衔 330 | // set_group_add_request 处理加群请求/邀请 331 | // get_stranger_info 获取陌生人信息 332 | // get_group_honor_info 获取群荣誉信息 333 | // get_record 获取语音 334 | // get_image 获取图片 335 | // can_send_image 检查是否可以发送图片 336 | // can_send_record 检查是否可以发送语音 337 | // get_status 获取运行状态 338 | // get_version_info 获取版本信息 339 | // set_restart 重启 OneBot 实现 340 | // clean_cache 清理缓存 341 | 342 | } 343 | 344 | for(let item of Api.api){ 345 | oneBot11API[item.url] = item.handle 346 | } 347 | 348 | module.exports = { 349 | oneBot11API 350 | } 351 | -------------------------------------------------------------------------------- /src/qqnt/api.js: -------------------------------------------------------------------------------- 1 | const {Log} = require("../logger"); 2 | const {Data, Setting} = require("../main/core"); 3 | const { QQNtAPI } = require('../qqnt/QQNtAPI'); 4 | const MessageModel = require("../oneBot11/messageModel"); 5 | const utils = require("../utils"); 6 | 7 | class BaseApi{ 8 | constructor(...cmdNames){ 9 | this.cmdNames = cmdNames; 10 | } 11 | handle(cmdObject){ 12 | 13 | } 14 | } 15 | 16 | /** 17 | * 收到消息 18 | */ 19 | class ReceiveMessage extends BaseApi{ 20 | constructor(){ super('onRecvMsg', "nodeIKernelMsgListener/onRecvMsg", "nodeIKernelMsgListener/onRecvActiveMsg"); } 21 | handle(cmdObject){ 22 | const messages = cmdObject?.payload?.msgList; 23 | if(messages){ 24 | MessageModel.handleNewMessage(messages).then(() => { }, (err) => { 25 | Log.e("解析消息失败: " + err.stack + '\n消息内容: ' + JSON.stringify(messages)); 26 | }) 27 | } 28 | } 29 | } 30 | 31 | 32 | /** 33 | * 监听自己发送的消息,用于获取msgId 34 | */ 35 | class SendMessage extends BaseApi{ 36 | constructor(){ super("nodeIKernelMsgListener/onAddSendMsg"); } 37 | handle(cmdObject){ 38 | /** @type QQNTMessage */ 39 | const msgRecord = cmdObject.payload?.msgRecord; 40 | if(msgRecord){ 41 | if(msgRecord.peerUid in QQNtAPI.sendMessageCallback){ 42 | QQNtAPI.sendMessageCallback[msgRecord.peerUid](msgRecord); 43 | delete QQNtAPI.sendMessageCallback[msgRecord.peerUid]; 44 | } 45 | } 46 | } 47 | } 48 | 49 | 50 | /** 51 | * 更新好友信息 52 | */ 53 | class UpdateFriendList extends BaseApi{ 54 | constructor(){ super("onBuddyListChange", "nodeIKernelBuddyListener/onBuddyListChange"); } 55 | handle(cmdObject){ 56 | const data = cmdObject?.payload?.data; 57 | if(!data) return; 58 | 59 | const friends = {}; 60 | const userMap = {}; 61 | 62 | data.forEach((category) => { 63 | const buddyList = category?.buddyList; 64 | if(buddyList) buddyList.forEach((friend) => { 65 | friends[friend.uin] = friend; 66 | userMap[friend.uid] = friend.uin; 67 | }) 68 | }); 69 | 70 | const friendsCount = Object.keys(friends).length; 71 | if(friendsCount === 0) return; 72 | 73 | Log.i(`加载 ${friendsCount} 个好友.`); 74 | 75 | Data.friends = friends; 76 | Data.userMap = userMap; 77 | } 78 | } 79 | 80 | 81 | /** 82 | * 更新群信息,包括群人数变动 83 | */ 84 | class UpdateGroupList extends BaseApi{ 85 | constructor(){ super("onGroupListUpdate", "nodeIKernelGroupListener/onGroupListUpdate"); } 86 | handle(cmdObject){ 87 | const groupList = cmdObject?.payload?.groupList; 88 | if(!groupList || groupList.length == 0) return; 89 | 90 | const type = cmdObject.payload?.updateType; 91 | if(type == 2){ 92 | groupList.forEach((group) => { 93 | let oldGroup = Data.groups[group.groupCode] 94 | if(oldGroup.memberCount != group.memberCount){ 95 | new Promise(async() => { 96 | let isDecrease = oldGroup.memberCount > group.memberCount; 97 | let oldMembers = Data.groupMembers[group.groupCode]; 98 | 99 | await utils.wait(1000); 100 | await Data.__updateGroupMember(group.groupCode, group.memberCount); 101 | 102 | let newMembers = Data.groupMembers[group.groupCode]; 103 | 104 | let members = isDecrease ? 105 | Object.keys(oldMembers).filter(key => !Object.keys(newMembers).includes(key)) : 106 | Object.keys(newMembers).filter(key => !Object.keys(oldMembers).includes(key)); 107 | 108 | for(let member of members){ 109 | let uin = isDecrease ? oldMembers[member].uin : newMembers[member].uin 110 | Log.i(`群(${group.groupCode}) 成员(${uin}) ${ isDecrease ? "退群" : "入群"}`); 111 | MessageModel.postNoticeData({ 112 | time: Date.now() / 1000, 113 | notice_type: isDecrease ? "group_decrease" : "group_increase", 114 | sub_type: isDecrease ? "leave" : "invite", 115 | user_id: uin, 116 | group_id: group.groupCode, 117 | operator_id: uin 118 | }) 119 | } 120 | }).then().catch(Log.e) 121 | } 122 | Data.groups[group.groupCode] = group 123 | }); 124 | }else{ 125 | groupList.forEach((group) => Data.groups[group.groupCode] = group); 126 | Log.i(`更新 ${Object.keys(groupList).length} 个群聊.`); 127 | } 128 | } 129 | } 130 | 131 | 132 | /** 133 | * 撤回消息, msgType = 5, subMsgType = 4 134 | */ 135 | class MsgInfoListUpdate extends BaseApi{ 136 | constructor(){ super("nodeIKernelMsgListener/onMsgInfoListUpdate"); } 137 | 138 | handle(cmdObject){ 139 | let msgList = cmdObject?.payload?.msgList; 140 | if(!msgList) return; 141 | for(let msg of msgList){ 142 | for(let element of msg.elements){ 143 | if(element.elementType == 8 && element.grayTipElement.subElementType == 1){ 144 | MessageModel.recallMessage(msg).then(() => { }, (err) => { 145 | Log.e("解析撤回消息失败: " + err.stack + '\n消息内容: ' + JSON.stringify(msg)); 146 | }); 147 | break; 148 | } 149 | } 150 | } 151 | } 152 | } 153 | 154 | /** 155 | * 更新好友请求列表 156 | * 157 | * @typedef friendRequest 158 | * @property {boolean} isDecide - 是否已处理 159 | * @property {boolean} isUnread - 是否未读 160 | * @property {boolean} isInitiator 161 | * @property {boolean} isShowCard 162 | * @property {boolean} isDoubt 163 | * @property {boolean} isAgreed 164 | * 165 | * @property {string} friendUid - 好友Uid, 字母和数字组成的字符串 166 | * @property {string} friendNick - 好友昵称 167 | * @property {string} nameMore 168 | * @property {string} extWords - 加好友的请求消息 169 | * @property {string} reqTime - 加好友的时间戳 170 | * @property {string} friendAvatarUrl - 好友头像Url 171 | * @property {string} groupCode 172 | * 173 | * @property {int} reqType 174 | * @property {int} reqSubType 175 | * @property {int} flag 176 | * @property {int} preGroupingId 177 | * @property {int} commFriendNum 178 | * @property {int} curFriendMax 179 | * @property {int} sourceId 180 | * @property {int} relation 181 | * 182 | * @property {Object} isBuddy 183 | */ 184 | class FriendRequest extends BaseApi{ 185 | constructor(){ super("nodeIKernelBuddyListener/onBuddyReqChange"); } 186 | 187 | handle(cmdObject){ 188 | cmdObject?.payload?.data?.buddyReqs 189 | ?.filter(friendRequest => friendRequest.isUnread && !friendRequest.isDecide) 190 | .forEach(friendRequest => { 191 | QQNtAPI.getUserInfoByUid(friendRequest.friendUid).then(info => { 192 | if(Setting.setting.setting.autoAcceptFriendRequest){ 193 | QQNtAPI.ntCall( 194 | "ns-ntApi", 195 | "nodeIKernelBuddyService/approvalFriendRequest", 196 | [{ 197 | "approvalInfo":{ 198 | "friendUid": friendRequest.friendUid, 199 | "accept": true 200 | } 201 | }, null] 202 | ).then(); 203 | } 204 | MessageModel.postRequestData({ 205 | request_type: 'friend', 206 | user_id: info.uin, 207 | comment: friendRequest.extWords, 208 | flag: friendRequest.friendUid 209 | }) 210 | }) 211 | }); 212 | } 213 | } 214 | 215 | /** 216 | * 媒体文件下载完成 217 | */ 218 | class FileDownloadComplete extends BaseApi{ 219 | constructor(){ super("nodeIKernelMsgListener/onRichMediaDownloadComplete"); } 220 | 221 | handle(cmdObject){ 222 | MessageModel.postNoticeData({ 223 | notice_type: "download_finish", 224 | file: { 225 | msgId: cmdObject.payload.notifyInfo.msgId, 226 | filePath: cmdObject.payload.notifyInfo.filePath, 227 | totalSize: cmdObject.payload.notifyInfo.totalSize, 228 | } 229 | }) 230 | } 231 | } 232 | 233 | 234 | /** 235 | * 刷新频道个人资料 236 | */ 237 | class RefreshGuildInfo extends BaseApi{ 238 | constructor(){ super('nodeIKernelGuildListener/onRefreshGuildUserProfileInfo'); } 239 | handle(cmdObject){ 240 | let info = cmdObject?.payload?.profileInfo; 241 | if(info){ 242 | Data.guildInfo.nickname = info.nick; 243 | Data.guildInfo.tiny_id = info.tinyId; 244 | // Data.guildInfo.avatar_url = ; 245 | } 246 | } 247 | } 248 | 249 | 250 | /** 251 | * 禁用更新提示 252 | */ 253 | class DisableUpdate extends BaseApi{ 254 | constructor(){ super("nodeIKernelUnitedConfigListener/onUnitedConfigUpdate"); } 255 | 256 | handle(cmdObject){ 257 | if(Setting.setting.misc.disableUpdate){ 258 | cmdObject.payload.configData.content = ""; 259 | cmdObject.payload.configData.isSwitchOn = false; 260 | } 261 | } 262 | } 263 | 264 | 265 | module.exports = { 266 | api: [ 267 | new ReceiveMessage(), 268 | new SendMessage(), 269 | new MsgInfoListUpdate(), 270 | 271 | new UpdateFriendList(), 272 | new UpdateGroupList(), 273 | 274 | new FileDownloadComplete(), 275 | 276 | new FriendRequest(), 277 | 278 | new RefreshGuildInfo(), 279 | 280 | new DisableUpdate() 281 | ] 282 | } -------------------------------------------------------------------------------- /src/qqnt/QQNtAPI.js: -------------------------------------------------------------------------------- 1 | const crypto = require("crypto"); 2 | const { ipcMain } = require("electron"); 3 | const {Log} = require("../logger"); 4 | 5 | class QQNtAPI { 6 | 7 | ntCallCallback = {}; 8 | 9 | sendMessageCallback = {}; 10 | 11 | ntCallAsyncCallback = {}; 12 | 13 | ntCall(eventName, cmdName, args, webContentsId = '2') { 14 | return new Promise((resolve, reject) => { 15 | const uuid = crypto.randomUUID(); 16 | this.ntCallCallback[uuid] = resolve; 17 | setTimeout(() => { 18 | if(this.ntCallCallback[uuid]){ 19 | delete this.ntCallCallback[uuid]; 20 | Log.e(`call QQNtAPI timeout: eventName=${eventName}, cmdName=${cmdName}`); 21 | reject('timeout'); 22 | } 23 | }, 5000) 24 | ipcMain.emit( 25 | `IPC_UP_${webContentsId}`, 26 | {}, // IpcMainEvent 27 | {type: 'request', callbackId: uuid, eventName: `${eventName}-${webContentsId}`}, 28 | [cmdName, ...args], 29 | ); 30 | }) 31 | } 32 | 33 | /** 34 | * 具有异步返还结果的ntCall 35 | * @param eventName 36 | * @param cmdName 37 | * @param args {Array} 38 | * @param {string} callBackCmdName 回调的CmdName 39 | * @param {(Object) => Boolean} isMyResult 判断收到的消息是否为需要的消息 40 | * @param {boolean} registerAfterCall 是否在调用之前注册回调 41 | * @param {(Object) => Object} afterCallback 在Promise回调之前执行的操作(此时已经拿到了请求结果) 42 | * @param {string} webContentsId 43 | * @return {Promise} 44 | */ 45 | ntCallAsync(eventName, cmdName, args= [], callBackCmdName, 46 | isMyResult = () => { return true }, 47 | registerAfterCall = false, 48 | webContentsId = '2', 49 | afterCallback = (obj) => { return obj }, 50 | ){ 51 | return new Promise((resolve, reject) => { 52 | const uuid = crypto.randomUUID(); 53 | 54 | function IsMyResult(cmdObject){ 55 | if(isMyResult(cmdObject)){ 56 | resolve(afterCallback(cmdObject)); 57 | return true; 58 | }else{ 59 | return false; 60 | } 61 | } 62 | 63 | if(!this.ntCallAsyncCallback[callBackCmdName]) this.ntCallAsyncCallback[callBackCmdName] = { }; 64 | 65 | if(!registerAfterCall) this.ntCallAsyncCallback[callBackCmdName][uuid] = IsMyResult; 66 | 67 | this.ntCallCallback[uuid] = (cmdObject) => { 68 | if(!cmdObject || cmdObject.result == 0){ 69 | if(registerAfterCall) this.ntCallAsyncCallback[callBackCmdName][uuid] = IsMyResult; 70 | }else{ 71 | Log.e(`call QQNtAPI failed: eventName=${eventName}, cmdName=${cmdName}`); 72 | reject('failed'); 73 | } 74 | }; 75 | 76 | setTimeout(() => { 77 | if(this.ntCallCallback[uuid] || this.ntCallAsyncCallback[callBackCmdName][uuid]){ 78 | if(this.ntCallCallback[uuid]) delete this.ntCallCallback[uuid]; 79 | if(this.ntCallAsyncCallback[callBackCmdName][uuid]) delete this.ntCallAsyncCallback[callBackCmdName][uuid]; 80 | Log.e(`call QQNtAPI timeout: eventName=${eventName}, cmdName=${cmdName}`); 81 | reject('timeout'); 82 | } 83 | }, 5000); 84 | 85 | ipcMain.emit( 86 | `IPC_UP_${webContentsId}`, 87 | {}, // IpcMainEvent 88 | {type: 'request', callbackId: uuid, eventName: eventName + "-" + webContentsId}, 89 | [cmdName, ...args], 90 | ); 91 | }) 92 | } 93 | 94 | ntCallBack(args, cmdObject){ 95 | if(args[0]?.callbackId){ 96 | const id = args[0].callbackId; 97 | if(id in this.ntCallCallback){ 98 | this.ntCallCallback[id](args[1]); 99 | delete this.ntCallCallback[id]; 100 | return true; 101 | } 102 | } 103 | const cmdName = cmdObject?.cmdName; 104 | if(cmdName){ 105 | if(cmdName in this.ntCallAsyncCallback){ 106 | for(let uuid in this.ntCallAsyncCallback[cmdName]){ 107 | if(this.ntCallAsyncCallback[cmdName][uuid](cmdObject)){ 108 | delete this.ntCallAsyncCallback[cmdName][uuid]; 109 | return true; 110 | } 111 | } 112 | } 113 | } 114 | return false 115 | } 116 | 117 | /** 118 | * 从网络拉取最新的用户信息 119 | */ 120 | getUserInfoByUid(uid) { 121 | return this.ntCallAsync( 122 | "ns-ntApi", "nodeIKernelProfileService/getUserDetailInfo", 123 | [{"uid": uid.toString()}, undefined], 124 | "nodeIKernelProfileListener/onProfileDetailInfoChanged", 125 | (cmdObject) => { 126 | let info = cmdObject.payload?.info 127 | if(!info) return false 128 | return info?.uid == uid && info?.uin != "0" 129 | }, 130 | false, 131 | "2", 132 | (obj) => { return obj.payload.info } 133 | ) 134 | } 135 | 136 | /** 137 | * 获取缓存的频道列表 138 | */ 139 | async getGuildList(){ 140 | let guilds = {}; 141 | let data = await this.ntCall("ns-ntApi", "nodeIKernelGuildService/getGuildAndChannelListFromCache", []); 142 | if(data?.guildList){ 143 | for(let guild of data.guildList){ 144 | let guildId = guild.guildId || guild.guild_id 145 | guilds[guildId] = { 146 | guild_id: guildId, 147 | guild_name: guild.name || guild.guild_info.guild_name, 148 | guild_display_id: guild.showNumber || guild.guild_info.guild_code, 149 | guild_profile: guild.profile || guild.guild_info.profile, 150 | create_time: guild.createTime || 0, 151 | max_member_count: guild.userMaxNum || 0, 152 | max_robot_count: guild.robotMaxNum || 0, 153 | max_admin_count: guild.adminMaxNum || 0, 154 | member_count: guild.userNum || 0, 155 | owner_id: "" 156 | } 157 | } 158 | } 159 | if(data?.guildInitList){ 160 | for(let guild of data.guildInitList){ 161 | let channelList = []; 162 | for(let category of guild.categoryList){ 163 | channelList = channelList.concat(category.channelList) 164 | } 165 | guilds[guild.guildId].channel_list = channelList; 166 | } 167 | } 168 | return guilds; 169 | } 170 | 171 | /** 172 | * 发送消息 173 | * @param peer 174 | * @param messages 175 | * @return {Promise} 176 | */ 177 | sendMessage(peer, messages) { 178 | return new Promise((resolve) => { 179 | this.sendMessageCallback[peer.peerUid] = (qqNtMsg) => { 180 | resolve(qqNtMsg) 181 | }; 182 | this.ntCall("ns-ntApi", "nodeIKernelMsgService/sendMsg", [{ 183 | msgId: "0", 184 | peer: peer, 185 | msgElements: messages, 186 | msgAttributeInfos: new Map() 187 | }, null]).then(); 188 | }); 189 | } 190 | 191 | /** 192 | * 获取群成员列表,有可能为空 193 | * @param groupId {string} 群号 194 | * @param num {number} 成员数量 195 | * @return {Promise>} 196 | */ 197 | async getGroupMembers(groupId, num) { 198 | let sceneId = await this.ntCall("ns-ntApi", "nodeIKernelGroupService/createMemberListScene", 199 | [{groupCode: groupId, scene: "groupMemberList_MainWindow"}] 200 | ) 201 | let res = await this.ntCall("ns-ntApi", "nodeIKernelGroupService/getNextMemberList", 202 | [{sceneId: sceneId, num: num}, null] 203 | ); 204 | 205 | return res.result.infos; 206 | } 207 | 208 | /** 209 | * 获取合并转发的消息 210 | */ 211 | async getMultiMessages(peer, rootId, msgId) { 212 | let js = await this.ntCall("ns-ntApi", "nodeIKernelMsgService/getMultiMsg", [{ 213 | peer: peer, 214 | rootMsgId: rootId, 215 | parentMsgId: msgId ? msgId : rootId 216 | }, null]) 217 | return js.msgList; 218 | } 219 | } 220 | 221 | QQNtAPI = new QQNtAPI() 222 | 223 | module.exports = { 224 | QQNtAPI 225 | } -------------------------------------------------------------------------------- /src/lib/event-target.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { kForOnEventAttribute, kListener } = require('./constants'); 4 | 5 | const kCode = Symbol('kCode'); 6 | const kData = Symbol('kData'); 7 | const kError = Symbol('kError'); 8 | const kMessage = Symbol('kMessage'); 9 | const kReason = Symbol('kReason'); 10 | const kTarget = Symbol('kTarget'); 11 | const kType = Symbol('kType'); 12 | const kWasClean = Symbol('kWasClean'); 13 | 14 | /** 15 | * Class representing an event. 16 | */ 17 | class Event { 18 | /** 19 | * Create a new `Event`. 20 | * 21 | * @param {String} type The name of the event 22 | * @throws {TypeError} If the `type` argument is not specified 23 | */ 24 | constructor(type) { 25 | this[kTarget] = null; 26 | this[kType] = type; 27 | } 28 | 29 | /** 30 | * @type {*} 31 | */ 32 | get target() { 33 | return this[kTarget]; 34 | } 35 | 36 | /** 37 | * @type {String} 38 | */ 39 | get type() { 40 | return this[kType]; 41 | } 42 | } 43 | 44 | Object.defineProperty(Event.prototype, 'target', { enumerable: true }); 45 | Object.defineProperty(Event.prototype, 'type', { enumerable: true }); 46 | 47 | /** 48 | * Class representing a close event. 49 | * 50 | * @extends Event 51 | */ 52 | class CloseEvent extends Event { 53 | /** 54 | * Create a new `CloseEvent`. 55 | * 56 | * @param {String} type The name of the event 57 | * @param {Object} [options] A dictionary object that allows for setting 58 | * attributes via object members of the same name 59 | * @param {Number} [options.code=0] The status code explaining why the 60 | * connection was closed 61 | * @param {String} [options.reason=''] A human-readable string explaining why 62 | * the connection was closed 63 | * @param {Boolean} [options.wasClean=false] Indicates whether or not the 64 | * connection was cleanly closed 65 | */ 66 | constructor(type, options = {}) { 67 | super(type); 68 | 69 | this[kCode] = options.code === undefined ? 0 : options.code; 70 | this[kReason] = options.reason === undefined ? '' : options.reason; 71 | this[kWasClean] = options.wasClean === undefined ? false : options.wasClean; 72 | } 73 | 74 | /** 75 | * @type {Number} 76 | */ 77 | get code() { 78 | return this[kCode]; 79 | } 80 | 81 | /** 82 | * @type {String} 83 | */ 84 | get reason() { 85 | return this[kReason]; 86 | } 87 | 88 | /** 89 | * @type {Boolean} 90 | */ 91 | get wasClean() { 92 | return this[kWasClean]; 93 | } 94 | } 95 | 96 | Object.defineProperty(CloseEvent.prototype, 'code', { enumerable: true }); 97 | Object.defineProperty(CloseEvent.prototype, 'reason', { enumerable: true }); 98 | Object.defineProperty(CloseEvent.prototype, 'wasClean', { enumerable: true }); 99 | 100 | /** 101 | * Class representing an error event. 102 | * 103 | * @extends Event 104 | */ 105 | class ErrorEvent extends Event { 106 | /** 107 | * Create a new `ErrorEvent`. 108 | * 109 | * @param {String} type The name of the event 110 | * @param {Object} [options] A dictionary object that allows for setting 111 | * attributes via object members of the same name 112 | * @param {*} [options.error=null] The error that generated this event 113 | * @param {String} [options.message=''] The error message 114 | */ 115 | constructor(type, options = {}) { 116 | super(type); 117 | 118 | this[kError] = options.error === undefined ? null : options.error; 119 | this[kMessage] = options.message === undefined ? '' : options.message; 120 | } 121 | 122 | /** 123 | * @type {*} 124 | */ 125 | get error() { 126 | return this[kError]; 127 | } 128 | 129 | /** 130 | * @type {String} 131 | */ 132 | get message() { 133 | return this[kMessage]; 134 | } 135 | } 136 | 137 | Object.defineProperty(ErrorEvent.prototype, 'error', { enumerable: true }); 138 | Object.defineProperty(ErrorEvent.prototype, 'message', { enumerable: true }); 139 | 140 | /** 141 | * Class representing a message event. 142 | * 143 | * @extends Event 144 | */ 145 | class MessageEvent extends Event { 146 | /** 147 | * Create a new `MessageEvent`. 148 | * 149 | * @param {String} type The name of the event 150 | * @param {Object} [options] A dictionary object that allows for setting 151 | * attributes via object members of the same name 152 | * @param {*} [options.data=null] The message content 153 | */ 154 | constructor(type, options = {}) { 155 | super(type); 156 | 157 | this[kData] = options.data === undefined ? null : options.data; 158 | } 159 | 160 | /** 161 | * @type {*} 162 | */ 163 | get data() { 164 | return this[kData]; 165 | } 166 | } 167 | 168 | Object.defineProperty(MessageEvent.prototype, 'data', { enumerable: true }); 169 | 170 | /** 171 | * This provides methods for emulating the `EventTarget` interface. It's not 172 | * meant to be used directly. 173 | * 174 | * @mixin 175 | */ 176 | const EventTarget = { 177 | /** 178 | * Register an event listener. 179 | * 180 | * @param {String} type A string representing the event type to listen for 181 | * @param {(Function|Object)} handler The listener to add 182 | * @param {Object} [options] An options object specifies characteristics about 183 | * the event listener 184 | * @param {Boolean} [options.once=false] A `Boolean` indicating that the 185 | * listener should be invoked at most once after being added. If `true`, 186 | * the listener would be automatically removed when invoked. 187 | * @public 188 | */ 189 | addEventListener(type, handler, options = {}) { 190 | for (const listener of this.listeners(type)) { 191 | if ( 192 | !options[kForOnEventAttribute] && 193 | listener[kListener] === handler && 194 | !listener[kForOnEventAttribute] 195 | ) { 196 | return; 197 | } 198 | } 199 | 200 | let wrapper; 201 | 202 | if (type === 'message') { 203 | wrapper = function onMessage(data, isBinary) { 204 | const event = new MessageEvent('message', { 205 | data: isBinary ? data : data.toString() 206 | }); 207 | 208 | event[kTarget] = this; 209 | callListener(handler, this, event); 210 | }; 211 | } else if (type === 'close') { 212 | wrapper = function onClose(code, message) { 213 | const event = new CloseEvent('close', { 214 | code, 215 | reason: message.toString(), 216 | wasClean: this._closeFrameReceived && this._closeFrameSent 217 | }); 218 | 219 | event[kTarget] = this; 220 | callListener(handler, this, event); 221 | }; 222 | } else if (type === 'error') { 223 | wrapper = function onError(error) { 224 | const event = new ErrorEvent('error', { 225 | error, 226 | message: error.message 227 | }); 228 | 229 | event[kTarget] = this; 230 | callListener(handler, this, event); 231 | }; 232 | } else if (type === 'open') { 233 | wrapper = function onOpen() { 234 | const event = new Event('open'); 235 | 236 | event[kTarget] = this; 237 | callListener(handler, this, event); 238 | }; 239 | } else { 240 | return; 241 | } 242 | 243 | wrapper[kForOnEventAttribute] = !!options[kForOnEventAttribute]; 244 | wrapper[kListener] = handler; 245 | 246 | if (options.once) { 247 | this.once(type, wrapper); 248 | } else { 249 | this.on(type, wrapper); 250 | } 251 | }, 252 | 253 | /** 254 | * Remove an event listener. 255 | * 256 | * @param {String} type A string representing the event type to remove 257 | * @param {(Function|Object)} handler The listener to remove 258 | * @public 259 | */ 260 | removeEventListener(type, handler) { 261 | for (const listener of this.listeners(type)) { 262 | if (listener[kListener] === handler && !listener[kForOnEventAttribute]) { 263 | this.removeListener(type, listener); 264 | break; 265 | } 266 | } 267 | } 268 | }; 269 | 270 | module.exports = { 271 | CloseEvent, 272 | ErrorEvent, 273 | Event, 274 | EventTarget, 275 | MessageEvent 276 | }; 277 | 278 | /** 279 | * Call an event listener 280 | * 281 | * @param {(Function|Object)} listener The listener to call 282 | * @param {*} thisArg The value to use as `this`` when calling the listener 283 | * @param {Event} event The event to pass to the listener 284 | * @private 285 | */ 286 | function callListener(listener, thisArg, event) { 287 | if (typeof listener === 'object' && listener.handleEvent) { 288 | listener.handleEvent.call(listener, event); 289 | } else { 290 | listener.call(thisArg, event); 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /src/main/core.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 模块核心 3 | */ 4 | 5 | const { Log } = require("../logger"); 6 | const { defaultSetting } = require("../common/const"); 7 | 8 | const utils = require("../utils"); 9 | const {LimitedHashMap} = require("../utils"); 10 | const { QQNtAPI } = require('../qqnt/QQNtAPI'); 11 | 12 | /** 13 | * 数据 14 | */ 15 | class Data{ 16 | 17 | /** 18 | * 自身信息 19 | */ 20 | static selfInfo = { 21 | account: "", 22 | uin: "", 23 | uid: "", 24 | }; 25 | 26 | static guildInfo = { 27 | nickname: "", 28 | tiny_id: "", 29 | avatar_url: "" 30 | } 31 | 32 | /** 33 | * uid -> QQ号 34 | * @type {Object.} 35 | */ 36 | static userMap = {}; 37 | 38 | 39 | /** 40 | * QQ号 -> 用户 41 | * @type {Object.} 42 | */ 43 | static friends = {}; 44 | 45 | 46 | static userInfo = new LimitedHashMap(100); 47 | 48 | /** 49 | * 群号 -> 群聊信息 50 | * @type {Object.} 51 | */ 52 | static groups = {}; 53 | 54 | /** 55 | * 频道 56 | */ 57 | static guilds = {}; 58 | 59 | 60 | /** 61 | * 群成员的信息 62 | * 群号 -> { 群成员列表 -> 群成员信息 } 63 | * @type {Object.>} 64 | */ 65 | static groupMembers = {}; 66 | 67 | /** 68 | * @typedef {Object} QQNTMessage 69 | * @property {string} msgId - 消息ID 70 | * @property {string} msgSeq - 消息序列号 71 | * @property {string} msgTime - 消息时间戳 72 | * 73 | * @property {number} chatType - 聊天类型 74 | * 75 | * @property {string} peerUid - 接收者UID 76 | * @property {string} senderUid - 发送者UID 77 | * 78 | * @property {Object} elements - 消息内容 79 | * 80 | * @property {number} msgType - 消息类型 81 | * @property {number} subMsgType - 子消息类型 82 | * @property {number} sendType - 发送类型 83 | * 84 | * @property {string} msgRandom - 消息随机数 85 | * @property {string} cntSeq - 计数序列号 86 | * 87 | * @property {string} fromUid - 来源UID 88 | * @property {string} fromAppid - 来源应用ID 89 | * 90 | * @property {string} msgMeta - 消息元数据 91 | * @property {string} sendRemarkName - 发送者备注名 92 | * @property {string} sendMemberName - 发送者成员名 93 | * @property {string} sendNickName - 发送者昵称 94 | * 95 | * @property {string} channelName - 频道名 96 | * @property {string} channelId - 频道ID 97 | * 98 | * @property {string} guildId - 公会/群组ID 99 | * @property {string} guildCode - 公会/群组代码 100 | * @property {string} guildName - 公会/群组名 101 | */ 102 | 103 | static historyMessage = new LimitedHashMap(1000); 104 | 105 | static pushHistoryMessage(/** @type QQNTMessage */ qqNtMsg, oneBotMsg){ 106 | this.historyMessage.put(qqNtMsg.msgId, { 107 | chatType: qqNtMsg.chatType, 108 | peerUid: qqNtMsg.peerUid, 109 | senderUid: qqNtMsg.senderUid, 110 | msgSeq: qqNtMsg.msgSeq, 111 | oneBotMsg: oneBotMsg 112 | }); 113 | } 114 | 115 | /** 116 | * 根据QQ号获取用户信息 117 | * @param {string} qq 118 | * @returns {User | null} 119 | */ 120 | static getInfoByQQ(qq){ 121 | let user = this.friends[qq?.toString()]; 122 | if(user){ 123 | return user; 124 | }else{ 125 | Log.e(`User with QQ ${qq} not found.`); 126 | return null; 127 | } 128 | } 129 | 130 | /** 131 | * 根据uid获取用户信息 132 | * @param {string} uid 133 | * @returns {User | null} 134 | */ 135 | static getInfoByUid(uid){ 136 | let qq = this.userMap[uid?.toString()]; 137 | if(qq){ 138 | return this.getInfoByQQ(qq); 139 | }else{ 140 | Log.e(`User with uid ${uid} not found.`); 141 | return null; 142 | } 143 | } 144 | 145 | /** 146 | * 根据群号获取群信息 147 | * @param {string} groupId 148 | * @return {Group | null} 149 | */ 150 | static getGroupById(groupId){ 151 | let group = this.groups[groupId?.toString()]; 152 | if(group){ 153 | return group; 154 | }else{ 155 | Log.e(`Group with uid ${groupId} not found.`); 156 | return null; 157 | } 158 | } 159 | 160 | 161 | /** 162 | * 根据 uid 获取群成员信息 163 | * @param groupId {string} 群号 164 | * @param uid {string} 用户uid 165 | * @return {Promise} 166 | */ 167 | static async getGroupMemberByUid(groupId, uid){ 168 | let members = await this.__getGroupMembers(groupId); 169 | return members[uid] || (Log.w(`getGroupMemberByUid: 用户 uid(${uid}) 在 群(${groupId}) 内不存在`), null); 170 | } 171 | 172 | 173 | /** 174 | * 根据 QQ号 获取群成员信息 175 | * @param groupId 群号 176 | * @param qq {string} 177 | * @param force 是否强制更新 178 | * @return {Promise} 179 | */ 180 | static async getGroupMemberByQQ(groupId, qq, force = false){ 181 | let members = await this.getGroupMemberList(groupId, force); 182 | let member = members.find(m => m.uin == qq); 183 | return member || (Log.w(`getGroupMemberByQQ: 用户 QQ(${qq}) 在 群(${groupId}) 内不存在`), null); 184 | } 185 | 186 | static async getGroupMemberInfo(groupId, qq, force = false){ 187 | let member = await this.getGroupMemberByQQ(groupId, qq, false); 188 | if(!member) return null; 189 | 190 | let info = this.userInfo[member.uin]; 191 | if(force || !info){ 192 | info = await QQNtAPI.getUserInfoByUid(member.uid); 193 | this.userInfo[member.uin] = info; 194 | } 195 | return info; 196 | } 197 | 198 | 199 | /** 200 | * 获取群成员列表 201 | * @param groupId 群号 202 | * @param force 是否强制更新 203 | * @return {Promise} 204 | */ 205 | static async getGroupMemberList(groupId, force = false) { 206 | let group = this.getGroupById(groupId); 207 | if(!group){ 208 | Log.w(`getGroupMemberList: 群(${groupId})不存在`); 209 | return []; 210 | } 211 | 212 | if(!force){ 213 | Log.d(`从缓存获取群成员列表`); 214 | let members = this.groupMembers[group.groupCode]; 215 | if(members){ 216 | let r = Object.values(members); 217 | if(r.length > 0){ 218 | Log.d(`返还缓存群成员列表`); 219 | return r; 220 | } 221 | } 222 | } 223 | Log.d(`联网刷新群成员列表 force = ${force}`); 224 | await this.__updateGroupMember(group.groupCode, group.memberCount) 225 | return Object.values(this.groupMembers[group.groupCode] || {}); 226 | } 227 | 228 | /** 229 | * 获取群成员列表 230 | * @return {Promise>} 231 | */ 232 | static async __getGroupMembers(groupId){ 233 | // 有缓存直接使用缓存 234 | let members = this.groupMembers[groupId]; 235 | if(members) return members; 236 | 237 | // 群不存在 238 | let group = this.getGroupById(groupId); 239 | if(!group) return {}; 240 | 241 | await this.__updateGroupMember(groupId, group.memberCount); 242 | return this.groupMembers[groupId] || {}; 243 | } 244 | 245 | // 更新群聊成员 246 | static async __updateGroupMember(groupId, num = 3000, retry = true){ 247 | Log.i(`尝试加载 群(${groupId}) 成员列表,加载${num}人`); 248 | 249 | let members = await QQNtAPI.getGroupMembers(groupId, num); 250 | 251 | if(members && members?.size > 0){ 252 | Log.i(`加载 群(${groupId}) 成员列表,共计${members.size}人`); 253 | let obj = {}; 254 | for(let [key, value] of members) obj[key] = value; 255 | this.groupMembers[groupId] = obj; 256 | }else if(retry){ 257 | Log.d(`重新尝试加载 群(${groupId}) 成员列表`); 258 | await utils.wait(1000); 259 | await this.__updateGroupMember(groupId, num, false); 260 | }else{ 261 | Log.e(`无法获取 群(${groupId}) 成员列表`) 262 | } 263 | } 264 | 265 | } 266 | 267 | /** 268 | * 系统设置 269 | */ 270 | class Setting{ 271 | static setting = defaultSetting; 272 | } 273 | 274 | /** 275 | * 上报模块 276 | */ 277 | class Reporter{ 278 | 279 | time = Date.now() / 1000; 280 | 281 | isLoaded = false; 282 | httpReporter = null; 283 | 284 | /** @type Function */ 285 | webSocketReporter = null; 286 | 287 | webSocketReverseReporter = null; 288 | 289 | /** 290 | * 上报event消息 291 | */ 292 | reportData(data){ 293 | if(!this.isLoaded) return; 294 | 295 | if(!Setting.setting.setting.reportOldMsg && data.time < this.time) return; 296 | 297 | if(Setting.setting.http.enable || Setting.setting.http.enableReport) this.__reportHttp(data); 298 | 299 | let str = JSON.stringify(data); 300 | if(Setting.setting.ws.enable) this.__reportWs(str); 301 | if(Setting.setting.wsReverse.enable) this.__reportWsReverse(str); 302 | 303 | } 304 | 305 | __reportHttp(data){ 306 | if(this.httpReporter) this.httpReporter(data); 307 | } 308 | 309 | __reportWs(str){ 310 | if(this.webSocketReporter) this.webSocketReporter(str); 311 | } 312 | 313 | __reportWsReverse(str){ 314 | if(this.webSocketReverseReporter) this.webSocketReverseReporter(str); 315 | } 316 | } 317 | 318 | module.exports = { 319 | Data, 320 | Setting, 321 | Reporter: new Reporter() 322 | }; 323 | -------------------------------------------------------------------------------- /src/renderer.js: -------------------------------------------------------------------------------- 1 | const ipcRenderer = window.OneBotApi.ipcRenderer_OneBot; 2 | 3 | const pluginPath = LiteLoader.plugins['OneBotApi-JS'].path.plugin; 4 | 5 | let settingHtml = ""; 6 | 7 | async function onConfigView(view){ 8 | 9 | } 10 | 11 | async function onSettingWindowCreated(view){ 12 | 13 | const IPCAction = OneBotApi.IPCAction; 14 | 15 | const configData = await OneBotApi.settingData(); 16 | 17 | if(settingHtml.length == 0){ 18 | settingHtml = await (await fetch(`local:///${pluginPath}/src/common/setting.html`)).text() 19 | } 20 | 21 | view.innerHTML = settingHtml; 22 | 23 | const wsStatus = view.querySelector('.ws #wsServerStatus'); 24 | const httpStatus = view.querySelector('.http #httpServerStatus'); 25 | 26 | const wsReverse = view.querySelector(".ws #wsReverseStatus"); 27 | const wsReverseApi = view.querySelector(".ws #wsReverseApiStatus"); 28 | const wsReverseEvent = view.querySelector(".ws #wsReverseEventStatus"); 29 | 30 | const wsPort = view.querySelector(".ws #wsPort"); 31 | const httpPort = view.querySelector(".http .HTTPPort"); 32 | const httpReport = view.querySelector(".http .HTTPReport"); 33 | 34 | const wsReverseUrl = view.querySelector(".ws #wsReverseUrl"); 35 | const wsReverseApiUrl = view.querySelector(".ws #wsReverseApiUrl"); 36 | const wsReverseEventUrl = view.querySelector(".ws #wsReverseEventUrl"); 37 | 38 | OneBotApi.invoke(IPCAction.ACTION_GET_FRIENDS).then(friends => { 39 | view.querySelector('.data #friendList').innerHTML = `共计: ${friends.length} 个好友`; 40 | }); 41 | 42 | OneBotApi.invoke(IPCAction.ACTION_GET_GROUPS).then(groups => { 43 | view.querySelector('.data #groupList').innerHTML = `共计: ${groups.length} 个群聊`; 44 | }); 45 | 46 | function updateServerStatus(){ 47 | OneBotApi.invoke(IPCAction.ACTION_SERVER_STATUS).then(data => { 48 | updateStatus(httpStatus, data.http); 49 | updateStatus(wsStatus, data.ws); 50 | 51 | updateStatus(wsReverse, data.wsReverse.wss); 52 | updateStatus(wsReverseApi, data.wsReverse.api); 53 | updateStatus(wsReverseEvent, data.wsReverse.event); 54 | }); 55 | } 56 | 57 | updateServerStatus(); 58 | 59 | function restartServerBtn(selector, label, action, data){ 60 | view.querySelector(selector).addEventListener("click", () => { 61 | label.innerHTML = "正在重启"; 62 | OneBotApi.send(action, data); 63 | setTimeout(updateServerStatus, 1000); 64 | }); 65 | } 66 | 67 | wsPort.value = configData.ws.port; 68 | httpPort.value = configData.http.port; 69 | 70 | httpReport.value = configData.http.host; 71 | 72 | wsReverseUrl.value = configData.wsReverse.url; 73 | wsReverseApiUrl.value = configData.wsReverse.apiUrl; 74 | wsReverseEventUrl.value = configData.wsReverse.eventUrl; 75 | 76 | // 启用HTTP API 77 | bindToggle(view, ".http #enableHttpServer", configData.http.enableServer, (enable) => { 78 | configData.http.enableServer = enable; 79 | OneBotApi.send(IPCAction.ACTION_SET_CONFIG, configData); 80 | setTimeout(updateServerStatus, 1000); 81 | }); 82 | 83 | // 重启HTTP API 按钮 84 | restartServerBtn('.http #restartHTTPServer', httpStatus, IPCAction.ACTION_RESTART_HTTP_SERVER, configData.http.port); 85 | 86 | // 重启Ws服务端按钮 87 | restartServerBtn('.ws #restartWsServer', wsStatus, IPCAction.ACTION_RESTART_WS_SERVER, configData.ws.port); 88 | 89 | // 启用HTTP上报 90 | bindToggle(view, ".http #enableHTTPReport", configData.http.enable || configData.http.enableReport, (enable) => { 91 | configData.http.enableReport = enable; 92 | OneBotApi.send(IPCAction.ACTION_SET_CONFIG, configData); 93 | }); 94 | 95 | // 应用HTTP端口设置 96 | bindButton(view, ".http #updateHTTPPort", () => { 97 | configData.http.port = parseInt(httpPort.value); 98 | httpStatus.innerHTML = "正在重启"; 99 | OneBotApi.send(IPCAction.ACTION_SET_CONFIG, configData); 100 | OneBotApi.send(IPCAction.ACTION_RESTART_HTTP_SERVER, configData.port); 101 | setTimeout(updateServerStatus, 1000); 102 | }); 103 | 104 | // 应用HTTP上报URL设置 105 | bindButton(view, ".http #updateHTTPReport", () => { 106 | configData.http.host = httpReport.value; 107 | OneBotApi.send('one_bot_api_set_config', configData); 108 | alert("设置成功"); 109 | }); 110 | 111 | // 启用正向Ws 112 | bindToggle(view, ".ws #enableWs", configData.ws.enable, (enable) => { 113 | configData.ws.enable = enable; 114 | OneBotApi.send(IPCAction.ACTION_SET_CONFIG, configData); 115 | if(enable){ 116 | wsStatus.innerHTML = "正在启动"; 117 | OneBotApi.send(IPCAction.ACTION_RESTART_WS_SERVER, configData.ws.port); 118 | }else{ 119 | wsStatus.innerHTML = "正在关闭"; 120 | OneBotApi.send(IPCAction.ACTION_STOP_WS_SERVER); 121 | } 122 | setTimeout(updateServerStatus, 1000); 123 | }); 124 | 125 | // 应用ws端口设置 126 | bindButton(view, ".ws #applyWsPort", () => { 127 | configData.ws.port = parseInt(wsPort.value); 128 | OneBotApi.send(IPCAction.ACTION_SET_CONFIG, configData); 129 | if(configData.ws.enable){ 130 | wsStatus.innerHTML = "正在重启"; 131 | OneBotApi.send(IPCAction.ACTION_RESTART_WS_SERVER, configData.ws.port); 132 | setTimeout(updateServerStatus, 1000); 133 | } 134 | }); 135 | 136 | // 启用反向Ws 137 | bindToggle(view, ".ws #enableWsReverse", configData.wsReverse.enable, (enable) => { 138 | configData.wsReverse.enable = enable; 139 | OneBotApi.send(IPCAction.ACTION_SET_CONFIG, configData); 140 | if(enable){ 141 | wsReverse.innerHTML = "正在重启"; 142 | wsReverseApi.innerHTML = "正在重启"; 143 | wsReverseEvent.innerHTML = "正在重启"; 144 | OneBotApi.send(IPCAction.ACTION_RESTART_WS_REVERSE_SERVER, configData.wsReverse); 145 | }else{ 146 | wsReverse.innerHTML = "正在关闭"; 147 | wsReverseApi.innerHTML = "正在关闭"; 148 | wsReverseEvent.innerHTML = "正在关闭"; 149 | OneBotApi.send(IPCAction.ACTION_STOP_WS_REVERSE_SERVER); 150 | } 151 | setTimeout(updateServerStatus, 3000); 152 | }); 153 | 154 | // 应用ws url 155 | bindButton(view, ".ws #applyWsReverseUrl", () => { 156 | configData.wsReverse.url = wsReverseUrl.value; 157 | OneBotApi.send(IPCAction.ACTION_SET_CONFIG, configData); 158 | if(configData.wsReverse.enable){ 159 | wsReverse.innerHTML = "正在重启"; 160 | OneBotApi.send(IPCAction.ACTION_RESTART_WS_REVERSE_SERVER, configData.wsReverse); 161 | setTimeout(updateServerStatus, 3000); 162 | } 163 | }); 164 | 165 | // 应用ws api url 166 | bindButton(view, ".ws #applyWsReverseApiUrl", () => { 167 | configData.wsReverse.apiUrl = wsReverseApiUrl.value; 168 | OneBotApi.send(IPCAction.ACTION_SET_CONFIG, configData); 169 | if(configData.wsReverse.enable){ 170 | wsReverseApi.innerHTML = "正在重启"; 171 | OneBotApi.send(IPCAction.ACTION_RESTART_WS_REVERSE_SERVER, configData.wsReverse); 172 | setTimeout(updateServerStatus, 3000); 173 | } 174 | }); 175 | 176 | // 应用ws event url 177 | bindButton(view, ".ws #applyWsReverseEventUrl", () => { 178 | configData.wsReverse.eventUrl = wsReverseEventUrl.value; 179 | OneBotApi.send(IPCAction.ACTION_SET_CONFIG, configData); 180 | if(configData.wsReverse.enable){ 181 | wsReverseEvent.innerHTML = "正在重启"; 182 | OneBotApi.send(IPCAction.ACTION_RESTART_WS_REVERSE_SERVER, configData.wsReverse); 183 | setTimeout(updateServerStatus, 1000); 184 | } 185 | }); 186 | 187 | bindButton(view, ".data #updateFriendList", () => { 188 | OneBotApi.invoke(IPCAction.ACTION_GET_FRIENDS).then(friends => { 189 | view.querySelector('.data #friendList').innerHTML = `共计: ${friends.length} 个好友`; 190 | }); 191 | }); 192 | 193 | bindButton(view, ".data #updateGroupList", () => { 194 | OneBotApi.invoke(IPCAction.ACTION_GET_GROUPS).then(groups => { 195 | view.querySelector('.data #groupList').innerHTML = `共计: ${groups.length} 个群聊`; 196 | }); 197 | }); 198 | 199 | // 上报自身消息 200 | bindToggle(view, ".setting #reportSelfMsg", configData.setting.reportSelfMsg, (enable) => { 201 | configData.setting.reportSelfMsg = enable; 202 | OneBotApi.send(IPCAction.ACTION_SET_CONFIG, configData); 203 | }); 204 | 205 | // 上报启动前的消息 206 | bindToggle(view, ".setting #reportOldMsg", configData.setting.reportOldMsg, (enable) => { 207 | configData.setting.reportOldMsg = enable; 208 | OneBotApi.send(IPCAction.ACTION_SET_CONFIG, configData); 209 | }); 210 | 211 | // 自动接受好友请求 212 | bindToggle(view, ".setting #autoAcceptFriendRequest", configData.setting.autoAcceptFriendRequest, (enable) => { 213 | configData.setting.autoAcceptFriendRequest = enable; 214 | OneBotApi.send(IPCAction.ACTION_SET_CONFIG, configData); 215 | }); 216 | 217 | bindToggle(view, ".misc #disableUpdate", configData.misc.disableUpdate, (enable) => { 218 | configData.misc.disableUpdate = enable; 219 | OneBotApi.send(IPCAction.ACTION_SET_CONFIG, configData); 220 | }); 221 | 222 | bindToggle(view, ".debug #debugMode", configData.debug.debug, (enable) => { 223 | configData.debug.debug = enable; 224 | OneBotApi.send(IPCAction.ACTION_SET_CONFIG, configData); 225 | }); 226 | 227 | bindToggle(view, ".debug #debugIPC", configData.debug.ipc, (enable) => { 228 | configData.debug.ipc = enable; 229 | OneBotApi.send(IPCAction.ACTION_SET_CONFIG, configData); 230 | }); 231 | 232 | bindButton(view, ".debug #get_group_msg_mask", () => { 233 | OneBotApi.invoke(IPCAction.ACTION_HTTP_TEST, 'get_group_msg_mask').then(result => { 234 | view.querySelector('.debug #apiTestResult').innerHTML = JSON.stringify(result, null, 2); 235 | }); 236 | }); 237 | } 238 | 239 | 240 | function bindButton(view, selector, callback){ 241 | view.querySelector(selector).addEventListener("click", callback); 242 | } 243 | 244 | function bindToggle(view, selector, value, callback){ 245 | const toggle = view.querySelector(selector); 246 | toggle.toggleAttribute("is-active", value); 247 | toggle.addEventListener("click", () => { 248 | callback(toggle.toggleAttribute("is-active")); 249 | }); 250 | } 251 | 252 | function updateStatus(view, data){ 253 | if(data.status) view.innerHTML = '运行中'; 254 | else if(data.msg) { 255 | view.title = data.msg; 256 | view.innerHTML = `${data.msg}`; 257 | }else view.innerHTML = '未运行'; 258 | } 259 | 260 | 261 | const url = location.href; 262 | if(url.includes("/index.html") && url.includes("#/main/message")){ 263 | OneBotApi.send('one_bot_api_load_main_page'); 264 | }else{ 265 | navigation.addEventListener("navigatesuccess", function func(event){ 266 | const url = event.target.currentEntry.url; 267 | // 检测是否为主界面 268 | if(url.includes("/index.html") && url.includes("#/main/message")){ 269 | navigation.removeEventListener("navigatesuccess", func); 270 | OneBotApi.send('one_bot_api_load_main_page') 271 | } 272 | }); 273 | } 274 | 275 | export { 276 | onConfigView, 277 | onSettingWindowCreated 278 | } -------------------------------------------------------------------------------- /doc/http.md: -------------------------------------------------------------------------------- 1 | # HTTP API 2 | 3 | 目前已实现的API如下表所示 4 | 5 | | URL | 是否支持 | 注释 | 6 | |-----------------------------------------------------------|:----:|---------------| 7 | | 消息处理 | | | 8 | | [`send_msg`](#send_msg) | ✓ | 发送消息 | 9 | | `send_private_msg` | ✓ | 发送私聊消息 | 10 | | `send_group_msg` | ✓ | 发送群消息 | 11 | | [`delete_msg`](#delete_msg) | ✓ | 撤回消息 | 12 | | [`get_msg`](#get_msg) | ✓ | 获取消息 | 13 | | [`get_record`](#get_record) | | 获取语音 | 14 | | [`get_image`](#get_image) | | 获取图片 | 15 | | [`get_forward_msg`](#get_forward_msg) | ✓ | 获取合并转发消息 | 16 | | [`can_send_image`](#can_send_image) | | 检查是否可以发送图片 | 17 | | [`can_send_record`](#can_send_record) | | 检查是否可以发送语音 | 18 | | 好友信息 | | | 19 | | [`get_login_info`](#get_login_info) | ✓ | 获取登录号信息 | 20 | | [`get_friend_list`](#get_friend_list) | ✓ | 获取好友列表 | 21 | | [`get_stranger_info`](#get_stranger_info) | | 获取陌生人信息 | 22 | | 群信息 | | | 23 | | [`get_group_list`](#get_group_list) | ✓ | 获取群列表 | 24 | | [`get_group_info`](#get_group_info) | ✓ | 获取群信息 | 25 | | [`get_group_member_list`](#get_group_member_list) | ✓ | 获取群成员列表 | 26 | | [`get_group_member_info`](#get_group_member_info) | ✓ | 获取群成员信息 | 27 | | 好友请求 | | | 28 | | [`set_friend_add_request`](#set_friend_add_request) | ✓ | 处理加好友请求 | 29 | | [`set_group_add_request`](#set_group_add_request) | | 处理加群请求/邀请 | 30 | | 好友操作 | | | 31 | | [`send_like`](#send_like) | ✓ | 发送好友赞 | 32 | | 群操作 | | | 33 | | [`set_group_kick`](#set_group_kick) | ✓ | 群组踢人 | 34 | | [`set_group_anonymous`](#set_group_anonymous) | | 群组匿名 | 35 | | [`set_group_admin`](#set_group_admin) | ✓ | 群组设置管理员 | 36 | | [`set_group_ban`](#set_group_ban) | ✓ | 群组单人禁言 | 37 | | [`set_group_whole_ban`](#set_group_whole_ban) | ✓ | 群组全员禁言 | 38 | | [`set_group_anonymous_ban`](#set_group_anonymous_ban) | | 群组匿名用户禁言 | 39 | | [`get_group_honor_info`](#get_group_honor_info) | | 获取群荣誉信息 | 40 | | [`set_group_name`](#set_group_name) | ✓ | 设置群名 | 41 | | [`set_group_special_title`](#set_group_special_title) | | 设置群组专属头衔 | 42 | | [`set_group_card`](#set_group_card) | ✓ | 设置群名片群备注 | 43 | | [`set_group_leave`](#set_group_leave) | ✓ | 退出群组 | 44 | | 频道 | | | 45 | | [`get_guild_service_profile`](#get_guild_service_profile) | ✓ | 获取频道系统内BOT的资料 | 46 | | [`get_guild_list`](#get_guild_list) | ✓ | 获取频道列表 | 47 | | [`send_guild_channel_msg`](#send_guild_channel_msg) | ✓ | 发送信息到子频道 | 48 | | 其他 | | | 49 | | [`get_status`](#get_status) | | 获取运行状态 | 50 | | [`get_version_info`](#get_version_info) | | 获取版本信息 | 51 | | [`set_restart`](#set_restart) | | 重启 OneBot 实现 | 52 | | [`clean_cache`](#clean_cache) | | 清理缓存 | 53 | 54 | 55 | ## `send_msg` 56 | ### 发送消息 57 | 目前 `send_msg`, `send_private_msg`, `send_group_msg` 三个接口可以通用,传入 `group_id` 发送群消息, 58 | 传入 `user_id` 发送私聊消息,优先判断 `group_id` 59 | 60 | **请求体** 61 | ```json lines 62 | { 63 | "user_id": "123456", // 目标QQ号, user_id 和 group_id 二选一,类型可以是 string 也可以是 int 64 | "group_id": "123456", // 目标群号 65 | "message": "Hello World" // 发送的消息,可以为字符串,也可以为消息数组 66 | } 67 | ``` 68 | **响应体** 69 | ```json lines 70 | { 71 | "status": "ok", 72 | "retcode": 0, 73 | "data": { 74 | "message_id": "123456" // 消息msgId 75 | } 76 | } 77 | ``` 78 | 79 | 80 | ## `delete_msg` 81 | ### 撤回消息 82 | 如有需要可以额外传入被撤回消息所属的 QQ群号 / 好友QQ号,这样即使发送的消息在Bot框架启动之前也可撤回,但是**msgId不存在的话会直接导致QQ崩溃** 83 | 84 | **请求体** 85 | 86 | ```json lines 87 | { 88 | "message_id": "123456", // 消息的msgId, 上报的消息和发送的消息均带有此参数 89 | "group_id": "123456", // (可选)消息所属的群 90 | "user_id": "123456", // (可选)消息所属的好友QQ 91 | } 92 | ``` 93 | **响应体** 94 | ```json lines 95 | { 96 | "status": "ok", 97 | "retcode": 0 98 | } 99 | ``` 100 | 101 | 102 | ## `get_msg` 103 | ### 获取消息 104 | 只支持获取Bot框架启动之后收到的消息,无法获取历史消息 105 | 106 | **请求体** 107 | 108 | ```json lines 109 | { 110 | "message_id": "123456", // 消息的msgId 111 | } 112 | ``` 113 | **响应体** 114 | ```json lines 115 | { 116 | "status": "ok", 117 | "retcode": 0, 118 | "data": { 119 | "time": 0, 120 | "self_id": "123456", 121 | "post_type": "message", 122 | "message_id": "123456", 123 | "message_type": "private", 124 | "sub_type": "friend", 125 | "user_id": "", 126 | "group_id": "", 127 | "message": [ ] 128 | } 129 | } 130 | ``` 131 | 132 | 133 | ## `get_forward_msg` 134 | 135 | ### 获取合并转发消息 136 | 如果发送的消息在Bot框架启动之前,可以额外传入被撤回消息所属的 QQ群号 / 好友QQ号 来获取消息内容 137 | 138 | **请求体** 139 | 140 | ```json lines 141 | { 142 | "id": "123456", // 消息的msgId, 上报的消息和发送的消息均带有此参数 143 | "group_id": "123456", // (可选)消息所属的群 144 | "user_id": "123456", // (可选)消息所属的好友QQ 145 | } 146 | ``` 147 | **响应体** 148 | ```json lines 149 | { 150 | "status": "ok", 151 | "retcode": 0, 152 | "data": { 153 | "message": [ 154 | { 155 | "type": "node", 156 | "data": { 157 | "user_id": "10001000", 158 | "content": [ 159 | {"type": "face", "data": {"id": "123"}}, 160 | {"type": "text", "data": {"text": "哈喽~"}} 161 | ] 162 | } 163 | } 164 | ] 165 | } 166 | } 167 | ``` 168 | 169 | ## `get_login_info` 170 | 171 | ### 获取账号信息 172 | 173 | **响应体** 174 | ```json lines 175 | { 176 | "status": "ok", 177 | "retcode": 0, 178 | "data": [ 179 | { 180 | "user_id": "123456" // QQ 号 181 | } 182 | ] 183 | } 184 | ``` 185 | 186 | 187 | ## `get_friend_list` 188 | ### 获取好友列表 189 | 190 | **响应体** 191 | ```json lines 192 | { 193 | "status": "ok", 194 | "retcode": 0, 195 | "data": [ 196 | { 197 | "user_id": "123456", // 好友QQ号 198 | "nickname": "", // 好友昵称 199 | "remark": "" // 好友备注 200 | } 201 | ] 202 | } 203 | ``` 204 | 205 | 206 | ## `get_group_list` 207 | ### 获取群列表 208 | 209 | **响应体** 210 | ```json lines 211 | { 212 | status: 'ok', 213 | retcode: 0, 214 | "data": [ 215 | { 216 | "group_id": "123456", // 群号 217 | "group_name": "", // 群名称 218 | "member_count": "", // 成员数 219 | "max_member_count": "" // 最大成员数(群容量) 220 | } 221 | ] 222 | } 223 | ``` 224 | 225 | 226 | ## `get_group_info` 227 | ### 获取群信息 228 | 229 | **请求体** 230 | ```json lines 231 | { 232 | "group_id": 123456, // 群号 233 | } 234 | ``` 235 | 236 | **响应体** 237 | ```json lines 238 | { 239 | status: 'ok', 240 | retcode: 0, 241 | "data": { 242 | "group_id": "123456", // 群号 243 | "group_name": "", // 群名称 244 | "member_count": "", // 成员数 245 | "max_member_count": "" // 最大成员数(群容量) 246 | } 247 | } 248 | ``` 249 | 250 | 251 | ## `get_group_member_list` 252 | ### 获取群成员列表 253 | 254 | **请求体** 255 | ```json lines 256 | { 257 | "group_id": 123456, // 群号 258 | "no_cache": false // 是否使用缓存 259 | } 260 | ``` 261 | 262 | **响应体** 263 | ```json lines 264 | { 265 | status: 'ok', 266 | retcode: 0, 267 | "data": [ 268 | { 269 | "group_id": 123456, // 群号 270 | "user_id": 123456, // QQ 号 271 | "nickname": "", // 昵称, 272 | "card": "", // 群名片/备注, 273 | "role": "", // 角色,owner 或 admin 或 member 274 | } 275 | ] 276 | } 277 | ``` 278 | 279 | ## `get_group_member_info` 280 | ### 获取群成员信息 281 | 282 | **请求体** 283 | ```json lines 284 | { 285 | "group_id": 123456, // 群号 286 | "user_id": 123456, // QQ号 287 | "no_cache": false // 是否使用缓存 288 | } 289 | ``` 290 | 291 | **响应体** 292 | ```json lines 293 | { 294 | status: 'ok', 295 | retcode: 0, 296 | "data": { 297 | "group_id": 123456, // 群号 298 | "user_id": 123456, // QQ 号 299 | "nickname": "", // 昵称, 300 | "card": "", // 群名片/备注, 301 | "level": 1, // 群等级 302 | "role": "", // 角色,owner 或 admin 或 member 303 | "sex": "male", // 性别,male 或 female 或 unknown 304 | "age": 1, // 年龄 305 | "area": "中国 浙江 杭州", // 所在地 306 | "raw": {} // 原始数据,包括群成员信息和用户信息 307 | } 308 | } 309 | ``` 310 | 311 | 312 | 313 | ## `set_friend_add_request` 314 | ### 处理好友请求 315 | 316 | **请求体** 317 | ```json lines 318 | { 319 | "flag": "xxxxx", // 上报的flag 320 | "approve": true // 是否接受 321 | } 322 | ``` 323 | 324 | ## `send_like` 325 | ### 发送好友赞 326 | 327 | **请求体** 328 | ```json lines 329 | { 330 | "user_id": "123456", // 对方 QQ 号 331 | "times": 1 // 赞的次数,每个好友每天最多 10 次 332 | } 333 | ``` 334 | 335 | ## `set_group_kick` 336 | ### 群组踢人 337 | **请求体** 338 | ```json lines 339 | { 340 | "group_id": "123456", // 群号 341 | "user_id": "123456", // 要踢的群成员QQ号 342 | "reject_add_request": false // 拒绝此人的加群请求 343 | } 344 | ``` 345 | 346 | ## `set_group_ban` 347 | ### 群组单人禁言 348 | **请求体** 349 | ```json lines 350 | { 351 | "group_id": "123456", // 群号 352 | "user_id": "123456", // 要禁言的群成员QQ号 353 | "duration": 60 // 禁言时长,单位秒,0 表示取消禁言 354 | } 355 | ``` 356 | 357 | ## `set_group_whole_ban` 358 | ### 群组全员禁言 359 | **请求体** 360 | ```json lines 361 | { 362 | "group_id": "123456", // 群号 363 | "enable": true, // 是否禁言 364 | } 365 | ``` 366 | 367 | ## `set_group_name` 368 | ### 设置群名 369 | **请求体** 370 | ```json lines 371 | { 372 | "group_id": "123456", // 群号 373 | "group_name": "群名称", // 新群名 374 | } 375 | ``` 376 | 377 | 378 | # 频道 379 | 频道API均参考 [Go-cqhttp](https://docs.go-cqhttp.org/api/guild.html) 文档 380 | ## `get_guild_service_profile` 381 | ### 获取频道系统内BOT的资料 382 | 383 | ## `get_guild_list` 384 | ### 获取频道列表 385 | 386 | ## `send_guild_channel_msg` 387 | ### 发送信息到子频道 388 | 发送消息前请先调用一次`get_guild_list`,否则会报错找不到频道 -------------------------------------------------------------------------------- /src/common/setting.html: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | HTTP API 22 | 正在获取 23 |
24 |
25 | 重新启动 26 |
27 |
28 |
29 | 30 | 31 | HTTP服务端端口 32 | 33 | 应用 34 | 35 | 36 | 37 |
38 | HTTP上报 39 | 是否开启HTTP上报服务 40 |
41 | 42 |
43 | 44 | HTTP上报URL 45 | 46 | 应用 47 | 48 |
49 | 50 |
51 |
52 | 53 | 54 | 55 | 56 | 57 | 服务状态 58 | 正在获取 59 | 重新启动 60 | 61 | 62 | 63 |
启用正向WebSocket
64 | 65 |
66 | 67 | 68 | 监听端口 69 | 70 | 应用 71 | 72 |
73 |
74 | 75 | 76 | 77 | 78 |
启用反向WebSocket
79 | 80 |
81 | 82 | 83 |
84 | 反向WebSocket URL 85 | 正在获取 86 |
87 | 88 | 应用并重启 89 |
90 | 91 | 92 |
93 | API URL 94 | 正在获取 95 |
96 | 97 | 应用并重启 98 |
99 | 100 | 101 |
102 | Event URL 103 | 正在获取 104 |
105 | 106 | 应用并重启 107 |
108 | 109 | 110 | 111 | 112 | 113 | 114 |
115 |
116 |
117 | 118 | 119 | 120 | 121 | 122 | 123 | Bot框架 124 | 未完成 125 | 重新加载 126 | 127 | 128 | 129 | 好友列表 130 | 正在获取 131 | 重新加载 132 | 133 | 134 | 135 | 群列表 136 | 正在获取 137 | 重新加载 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 |
150 | 上报自己的消息 151 | 上报自身发送的消息,不包括调用API发送的消息 152 |
153 | 154 |
155 | 156 | 157 |
158 | 上报QQ启动前的消息 159 | 上报QQ启动前收到的消息(仅包含收到的部分消息) 160 |
161 | 162 |
163 | 164 | 165 |
166 | 自动接受加好友请求 167 | 自动接受加好友请求 168 |
169 | 170 |
171 | 172 |
173 |
174 |
175 | 176 | 177 | 178 | 179 | 180 | 181 |
182 | 禁用更新 183 | 禁用QQ更新提示 184 |
185 | 186 |
187 |
188 |
189 |
190 | 191 | 192 | 193 | 194 | 195 | 196 |
197 | 调试模式 198 | 在终端输出调试信息 199 |
200 | 201 |
202 | 203 | 204 |
205 | 打印IPC通信 206 | 在终端输出IPC通信信息 207 |
208 | 209 |
210 | 211 |
212 |
213 | 214 | 215 | 216 | 217 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 获取群消息设置 230 | 231 | 232 | 237 | 238 | 239 | 240 | 241 |
242 | 243 | 244 | 245 | 246 | 247 | 248 |
249 | OneBotApi-JS 250 | 在LiteLoaderQQNT上添加OneBot协议支持 251 |
252 |
253 | 254 | 255 |
256 | 开源协议 257 | MIT License 258 |
259 |
260 | 261 |
262 |
263 |
264 | 265 | 266 | -------------------------------------------------------------------------------- /src/oneBot11/message.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const utils = require("../utils"); 3 | const { Data } = require('../main/core'); 4 | const { Log } = require("../logger"); 5 | const { QQNtAPI } = require('../qqnt/QQNtAPI'); 6 | 7 | /** 8 | * 消息的基类 9 | * @class Message 10 | */ 11 | class Message{ 12 | /** 13 | * @param type {string} 14 | * @param data {Object} 15 | */ 16 | constructor(type, data){ 17 | this.type = type 18 | this.data = data 19 | } 20 | 21 | toCqCode(){ 22 | return `[CQ:${this.type}]`; 23 | } 24 | 25 | /** 26 | * @return Object 27 | */ 28 | toQQNT(){ 29 | return { 30 | elementType: 1, 31 | elementId: "", 32 | textElement: { 33 | content: "", 34 | atType: 0, 35 | atUid: "", 36 | atTinyId: "", 37 | atNtUid: "", 38 | } 39 | } 40 | } 41 | 42 | /** 43 | * 从QQNTMsg创建OneBot11Message 44 | * @param QQNTMsg {QQNTMessage} 45 | * @param element 46 | * @return {Promise | Message} 47 | */ 48 | static parseFromQQNT(QQNTMsg, element){ 49 | return new Message("unsupportType",{raw: element}) 50 | } 51 | } 52 | 53 | 54 | class Text extends Message{ 55 | /** 56 | * @param text {string} 57 | */ 58 | constructor(text){ 59 | super("text", { 60 | text: text 61 | }); 62 | } 63 | 64 | toCqCode(){ 65 | return this.data.text; 66 | } 67 | 68 | toQQNT(){ 69 | return { 70 | elementType: 1, 71 | elementId: "", 72 | textElement: { 73 | content: this.data.text, 74 | atType: 0, 75 | atUid: "", 76 | atTinyId: "", 77 | atNtUid: "", 78 | } 79 | } 80 | } 81 | 82 | static parseFromQQNT(QQNTMsg, element){ 83 | return new Text(element.textElement.content) 84 | } 85 | } 86 | 87 | 88 | /** 89 | * 表情消息 90 | */ 91 | class Face extends Message{ 92 | 93 | /** 94 | * @param id {number} 95 | */ 96 | constructor(id){ 97 | super("face", { 98 | id: id 99 | }); 100 | } 101 | 102 | toCqCode(){ 103 | return `[CQ:face,id=${this.data.id}]`; 104 | } 105 | 106 | toQQNT(){ 107 | return { 108 | elementType: 6, 109 | elementId: "", 110 | faceElement: { 111 | faceIndex: this.data.id, 112 | faceType: 1, 113 | } 114 | } 115 | } 116 | 117 | static parseFromQQNT(QQNTMsg, element){ 118 | return new Face(element.faceElement.faceIndex) 119 | } 120 | } 121 | 122 | 123 | class At extends Message{ 124 | 125 | /** 126 | * @param atAll {boolean} 127 | * @param member {GroupMember | null} 128 | */ 129 | constructor(atAll = false, member = null){ 130 | super("at", { 131 | qq: atAll ? "all" : (member?.uin ? member.uin : "") 132 | }); 133 | this.member = member; 134 | } 135 | 136 | toCqCode(){ 137 | return `[CQ:at,qq=${this.data.qq}]`; 138 | } 139 | 140 | toQQNT(){ 141 | if(this.data.qq === 'all'){ 142 | return { 143 | elementType: 1, 144 | elementId: "", 145 | textElement: { 146 | "content": "@全体成员", 147 | "atType": 1, 148 | "atUid": "0" 149 | } 150 | } 151 | }else{ 152 | return { 153 | elementType: 1, 154 | elementId: "", 155 | textElement: { 156 | "content": this.member.cardName, 157 | "atType": 2, 158 | "atUid": this.member.uid, 159 | "atNtUid": this.member.uid, 160 | "atTinyId": "", 161 | } 162 | } 163 | } 164 | } 165 | 166 | static async createAt(group, qq){ 167 | if(group){ 168 | let member = await Data.getGroupMemberByQQ(group, qq); 169 | if(member){ 170 | return new At(false, member); 171 | }else{ 172 | throw `群成员 QQ(${qq}) 不在 群(${group}) 里`; 173 | } 174 | }else{ 175 | throw 'Must provide group' 176 | } 177 | } 178 | 179 | static async parseFromQQNT(QQNTMsg, element){ 180 | if(element.textElement.atType == 1){ 181 | return new At(true, null) 182 | }else if(element.textElement.atType == 2){ 183 | return new At(false, await Data.getGroupMemberByUid(QQNTMsg.peerUid, element.textElement.atNtUid)) 184 | }else{ 185 | return new At(false, null) 186 | } 187 | } 188 | } 189 | 190 | 191 | class Image extends Message{ 192 | 193 | /** 194 | * @param data {Object} 195 | * @param filePath 196 | * @param fileName 197 | * @param fileSize 198 | * @param width 199 | * @param height 200 | */ 201 | constructor(data, filePath = "", fileName = "", fileSize = 0, width = 0, height = 0,){ 202 | super("image", data); 203 | this.filePath = filePath; 204 | this.fileName = fileName; 205 | this.fileSize = fileSize; 206 | this.width = width; 207 | this.height = height; 208 | } 209 | 210 | toCqCode(){ 211 | return `[CQ:image,md5=${this.data.md5}]`; 212 | } 213 | 214 | toQQNT(){ 215 | return { 216 | elementType: 2, 217 | elementId: "", 218 | picElement: { 219 | md5HexStr: this.data.md5, 220 | fileSize: this.fileSize, 221 | picWidth: this.width, 222 | picHeight: this.height, 223 | fileName: this.fileName, 224 | sourcePath: this.filePath, 225 | original: true, 226 | picType: 1001, 227 | picSubType: 0, 228 | fileUuid: "", 229 | fileSubId: "", 230 | thumbFileSize: 0, 231 | summary: "", 232 | } 233 | }; 234 | } 235 | 236 | /** 237 | * @return {Promise | Promise} 238 | */ 239 | static async createImage(url){ 240 | let file; 241 | 242 | if(url.startsWith("file://")){ 243 | file = url.split("file://")[1]; 244 | 245 | }else if(url.startsWith("http://") || url.startsWith("https://")){ 246 | Log.e('暂不支持发送非本地图片'); 247 | return new Text("[图片]"); 248 | 249 | }else{ 250 | file = url; 251 | } 252 | 253 | if(!fs.existsSync(file)){ 254 | Log.e('发送图片失败,图片文件不存在'); 255 | return new Text("[图片]"); 256 | } 257 | 258 | const md5 = utils.md5(file); 259 | const ext = file.substring(file.lastIndexOf('.') + 1); 260 | const fileName = `${md5}.${ext}`; 261 | 262 | const filePath = await QQNtAPI.ntCall("ns-ntApi", "nodeIKernelMsgService/getRichMediaFilePathForGuild", [ 263 | { 264 | path_info: { 265 | md5HexStr: md5, 266 | fileName: fileName, 267 | elementType: 2, 268 | elementSubType: 0, 269 | thumbSize: 0, 270 | needCreate: true, 271 | downloadType: 1, 272 | file_uuid: "" 273 | } 274 | } 275 | ]); 276 | 277 | if(typeof filePath !== 'string' || filePath.trim() === ''){ 278 | Log.e(`发送图片失败,无法创建图片文件, path: ${filePath}, name: ${fileName}`); 279 | return new Text("[图片]"); 280 | } 281 | 282 | fs.copyFileSync(file, filePath, fs.constants.COPYFILE_FICLONE); 283 | 284 | const fileSize = fs.statSync(filePath).size; 285 | 286 | const imageSize = await QQNtAPI.ntCall("ns-FsApi", "getImageSizeFromPath", [filePath]); 287 | 288 | if(!imageSize?.width || !imageSize?.height){ 289 | Log.e(`发送图片失败,无法获取图片大小, path: ${filePath}, name: ${fileName}`); 290 | return new Text("[图片]"); 291 | } 292 | return new Image({ 293 | file: filePath.startsWith("/") ? "file://" : "file:///" + filePath, 294 | url: "", 295 | md5: md5, 296 | }, filePath, fileName, fileSize, imageSize.width, imageSize.height) 297 | } 298 | 299 | 300 | static async parseFromQQNT(QQNTMsg, element){ 301 | let picElement = element.picElement; 302 | 303 | // if(!fs.existsSync(picElement.sourcePath)){ 304 | // await QQNtAPI.ntCall("ns-ntApi", "nodeIKernelMsgService/downloadRichMedia", [{ 305 | // getReq: { 306 | // msgId: QQNTMsg.msgId, 307 | // elementId: element.elementId, 308 | // chatType: QQNTMsg.chatType, 309 | // peerUid: QQNTMsg.peerUid, 310 | // thumbSize: 0, 311 | // downloadType: 2, 312 | // filePath: picElement.thumbPath.get(0), 313 | // }, 314 | // }, undefined, 315 | // ]); 316 | // } 317 | 318 | return new Image({ 319 | file: picElement.sourcePath.startsWith("/") ? "file://" : "file:///" + picElement.sourcePath, 320 | url: 'https://c2cpicdw.qpic.cn' + picElement.originImageUrl, 321 | md5: picElement.md5HexStr.toUpperCase() 322 | }, picElement.sourcePath, picElement.fileName, picElement.fileSize, picElement.picWidth, picElement.picHeight) 323 | } 324 | } 325 | 326 | 327 | class File extends Message{ 328 | /** 329 | * @param data {Object} 330 | */ 331 | constructor(data){ 332 | super("file", data); 333 | } 334 | 335 | toCqCode(){ 336 | return `[CQ:file,id=${this.data.name}]`; 337 | } 338 | 339 | toQQNT(){ 340 | return new Text("").toQQNT() 341 | } 342 | 343 | static parseFromQQNT(QQNTMsg, element){ 344 | let fileElement = element.fileElement 345 | if(fileElement){ 346 | return new File({ 347 | name: fileElement.fileName, 348 | size: parseInt(fileElement.fileSize), 349 | elementId: element.elementId 350 | }) 351 | }else{ 352 | return new File({}) 353 | } 354 | } 355 | } 356 | 357 | 358 | class Reply extends Message{ 359 | /** 360 | * @param data {Object} 361 | */ 362 | constructor(data){ 363 | super("reply", data); 364 | } 365 | 366 | toCqCode(){ 367 | return `[CQ:reply,id=${this.data.id}]`; 368 | } 369 | 370 | toQQNT(){ 371 | let msg = Data.historyMessage.get(this.data.id); 372 | if(!msg) throw `无法找到回复的消息`; 373 | return { 374 | elementType: 7, 375 | elementId: "", 376 | replyElement: { 377 | "replayMsgId": this.data.id, 378 | "replayMsgSeq": msg.msgSeq, 379 | "sourceMsgText": "", 380 | "senderUid": msg.senderUid, 381 | "senderUidStr": msg.senderUid, 382 | "replyMsgClientSeq": "", 383 | "replyMsgTime": "", 384 | "replyMsgRevokeType": 0, 385 | "sourceMsgTextElems": [], 386 | "sourceMsgIsIncPic": false, 387 | "sourceMsgExpired": false 388 | } 389 | } 390 | } 391 | 392 | static parseFromQQNT(QQNTMsg, element){ 393 | let replyMsg = QQNTMsg.records?.[0]; 394 | if(replyMsg){ 395 | return new Reply({ 396 | id: replyMsg.msgId 397 | }) 398 | }else{ 399 | Log.w(`无法解析回复消息`); 400 | return new Reply({}) 401 | } 402 | } 403 | } 404 | 405 | 406 | /** 407 | * 窗口抖动 408 | */ 409 | class Shake extends Message{ 410 | 411 | } 412 | 413 | /** 414 | * Json消息 415 | */ 416 | class Ark extends Message{ 417 | /** 418 | * @param data {Object} 419 | */ 420 | constructor(data){ 421 | super("json", data); 422 | } 423 | 424 | toCqCode(){ 425 | return `[CQ:json,data=${this.data.data}]`; 426 | } 427 | 428 | toQQNT(){ 429 | return { 430 | elementType: 10, 431 | elementId: "", 432 | arkElement: { 433 | "bytesData": JSON.stringify(this.data.data), 434 | "linkInfo": null, 435 | "subElementType": null 436 | } 437 | } 438 | } 439 | 440 | static parseFromQQNT(QQNTMsg, element){ 441 | return new Ark({ data: element.arkElement.bytesData }) 442 | } 443 | } 444 | 445 | /** 446 | * 转发消息 447 | */ 448 | class Forward extends Message{ 449 | /** 450 | * @param data {Object} 451 | */ 452 | constructor(data){ 453 | super("forward", data); 454 | } 455 | 456 | toCqCode(){ 457 | return `[CQ:forward,id=${this.data.id}]`; 458 | } 459 | 460 | toQQNT(){ 461 | return new Text("[聊天记录]").toQQNT() 462 | } 463 | 464 | static parseFromQQNT(QQNTMsg, element){ 465 | return new Forward({ id: QQNTMsg.msgId }) 466 | } 467 | } 468 | 469 | 470 | class MarkDown extends Message{ 471 | 472 | constructor(content){ 473 | super("markdown", { 474 | content: content 475 | }); 476 | } 477 | 478 | toCqCode(){ 479 | return this.data.content; 480 | } 481 | 482 | toQQNT(){ 483 | return new Text("[MarkDown消息]").toQQNT() 484 | } 485 | 486 | static parseFromQQNT(QQNTMsg, element){ 487 | return new MarkDown(element.markdownElement.content) 488 | } 489 | } 490 | 491 | 492 | /** 493 | * 从Json中创建QQNTMessage 494 | * @param oneBotMsg {Object} 495 | * @param group_id {number | null} 496 | * @return Message 497 | */ 498 | async function createOneBot(oneBotMsg, group_id = null){ 499 | switch(oneBotMsg.type){ 500 | case 'text': 501 | return new Text(oneBotMsg.data.text); 502 | 503 | case 'face': 504 | return new Face(oneBotMsg.data.id); 505 | 506 | case 'at': 507 | if(oneBotMsg.data.qq == 'all'){ 508 | return new At(true); 509 | }else{ 510 | return (await At.createAt(group_id, oneBotMsg.data.qq)); 511 | } 512 | 513 | case 'image': 514 | return (await Image.createImage(oneBotMsg.data.file)); 515 | 516 | case 'file': 517 | return new File(oneBotMsg.data); 518 | 519 | case 'reply': 520 | return new Reply(oneBotMsg.data); 521 | 522 | case 'json': 523 | return new Ark(oneBotMsg.data.data); 524 | 525 | case 'markdown': 526 | return new MarkDown(oneBotMsg.data.content); 527 | 528 | // case 'shake': 529 | // return Shake.OneBot2QQNT(item); 530 | default: 531 | throw '无法解析的消息类型: ' + oneBotMsg.type; 532 | } 533 | } 534 | 535 | /** 536 | * @return {Promise} 537 | */ 538 | async function parseFromQQNT(QQNTMessage, element){ 539 | switch(element.elementType){ 540 | // 文本消息和At消息 541 | case 1: { 542 | let textElement = element.textElement; 543 | if(textElement.atType == 0){ 544 | return Text.parseFromQQNT(QQNTMessage, element); 545 | }else{ 546 | return await At.parseFromQQNT(QQNTMessage, element); 547 | } 548 | } 549 | 550 | // 图片消息 551 | case 2: return await Image.parseFromQQNT(QQNTMessage, element); 552 | 553 | // 文件消息 554 | case 3: return File.parseFromQQNT(QQNTMessage, element); 555 | 556 | // 表情消息 557 | case 6: 558 | let faceElement = element.faceElement; 559 | if(faceElement.faceType == 5) return Shake.parseFromQQNT(QQNTMessage, element); 560 | return Face.parseFromQQNT(QQNTMessage, element); 561 | 562 | // 回复消息 563 | case 7: return Reply.parseFromQQNT(QQNTMessage, element); 564 | 565 | // Json消息 566 | case 10: return Ark.parseFromQQNT(QQNTMessage, element); 567 | 568 | // MD消息 569 | case 14: return MarkDown.parseFromQQNT(QQNTMessage, element); 570 | 571 | // 转发消息 572 | case 16: return Forward.parseFromQQNT(QQNTMessage, element) 573 | 574 | default: 575 | return Message.parseFromQQNT(QQNTMessage, element) 576 | } 577 | } 578 | 579 | /** 580 | * 创建发送消息的对象 581 | * @param group_id 582 | * @param user_id 583 | * @return { { 584 | * peerUid: string, 585 | * guildId: string, 586 | * chatType: number 587 | * } | null } 588 | */ 589 | function createPeer(group_id, user_id){ 590 | if(group_id){ 591 | let group = Data.getGroupById(group_id); 592 | 593 | if(!group){ 594 | Log.e(`Unable to find group with ${group_id}`); 595 | return null; 596 | 597 | }else{ 598 | return { 599 | chatType: 2, 600 | peerUid: group.groupCode, 601 | guildId: "" 602 | } 603 | } 604 | }else if(user_id){ 605 | let friend = Data.getInfoByQQ(user_id); 606 | 607 | if(!friend){ 608 | Log.e(`Unable to find friend with QQ ${user_id}`); 609 | return null; 610 | 611 | }else{ 612 | return { 613 | chatType: 1, 614 | peerUid: friend.uid, 615 | guildId: "" 616 | } 617 | } 618 | }else{ 619 | return null; 620 | } 621 | } 622 | 623 | 624 | module.exports = { 625 | Text, 626 | Face, 627 | At, 628 | Image, 629 | File, 630 | Reply, 631 | 632 | createPeer, 633 | createOneBot, 634 | parseFromQQNT 635 | } -------------------------------------------------------------------------------- /src/lib/sender.js: -------------------------------------------------------------------------------- 1 | /* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^Duplex" }] */ 2 | 3 | 'use strict'; 4 | 5 | const { Duplex } = require('stream'); 6 | const { randomFillSync } = require('crypto'); 7 | 8 | const PerMessageDeflate = require('./permessage-deflate'); 9 | const { EMPTY_BUFFER } = require('./constants'); 10 | const { isValidStatusCode } = require('./validation'); 11 | const { mask: applyMask, toBuffer } = require('./buffer-util'); 12 | 13 | const kByteLength = Symbol('kByteLength'); 14 | const maskBuffer = Buffer.alloc(4); 15 | 16 | /** 17 | * HyBi Sender implementation. 18 | */ 19 | class Sender { 20 | /** 21 | * Creates a Sender instance. 22 | * 23 | * @param {Duplex} socket The connection socket 24 | * @param {Object} [extensions] An object containing the negotiated extensions 25 | * @param {Function} [generateMask] The function used to generate the masking 26 | * key 27 | */ 28 | constructor(socket, extensions, generateMask) { 29 | this._extensions = extensions || {}; 30 | 31 | if (generateMask) { 32 | this._generateMask = generateMask; 33 | this._maskBuffer = Buffer.alloc(4); 34 | } 35 | 36 | this._socket = socket; 37 | 38 | this._firstFragment = true; 39 | this._compress = false; 40 | 41 | this._bufferedBytes = 0; 42 | this._deflating = false; 43 | this._queue = []; 44 | } 45 | 46 | /** 47 | * Frames a piece of data according to the HyBi WebSocket protocol. 48 | * 49 | * @param {(Buffer|String)} data The data to frame 50 | * @param {Object} options Options object 51 | * @param {Boolean} [options.fin=false] Specifies whether or not to set the 52 | * FIN bit 53 | * @param {Function} [options.generateMask] The function used to generate the 54 | * masking key 55 | * @param {Boolean} [options.mask=false] Specifies whether or not to mask 56 | * `data` 57 | * @param {Buffer} [options.maskBuffer] The buffer used to store the masking 58 | * key 59 | * @param {Number} options.opcode The opcode 60 | * @param {Boolean} [options.readOnly=false] Specifies whether `data` can be 61 | * modified 62 | * @param {Boolean} [options.rsv1=false] Specifies whether or not to set the 63 | * RSV1 bit 64 | * @return {(Buffer|String)[]} The framed data 65 | * @public 66 | */ 67 | static frame(data, options) { 68 | let mask; 69 | let merge = false; 70 | let offset = 2; 71 | let skipMasking = false; 72 | 73 | if (options.mask) { 74 | mask = options.maskBuffer || maskBuffer; 75 | 76 | if (options.generateMask) { 77 | options.generateMask(mask); 78 | } else { 79 | randomFillSync(mask, 0, 4); 80 | } 81 | 82 | skipMasking = (mask[0] | mask[1] | mask[2] | mask[3]) === 0; 83 | offset = 6; 84 | } 85 | 86 | let dataLength; 87 | 88 | if (typeof data === 'string') { 89 | if ( 90 | (!options.mask || skipMasking) && 91 | options[kByteLength] !== undefined 92 | ) { 93 | dataLength = options[kByteLength]; 94 | } else { 95 | data = Buffer.from(data); 96 | dataLength = data.length; 97 | } 98 | } else { 99 | dataLength = data.length; 100 | merge = options.mask && options.readOnly && !skipMasking; 101 | } 102 | 103 | let payloadLength = dataLength; 104 | 105 | if (dataLength >= 65536) { 106 | offset += 8; 107 | payloadLength = 127; 108 | } else if (dataLength > 125) { 109 | offset += 2; 110 | payloadLength = 126; 111 | } 112 | 113 | const target = Buffer.allocUnsafe(merge ? dataLength + offset : offset); 114 | 115 | target[0] = options.fin ? options.opcode | 0x80 : options.opcode; 116 | if (options.rsv1) target[0] |= 0x40; 117 | 118 | target[1] = payloadLength; 119 | 120 | if (payloadLength === 126) { 121 | target.writeUInt16BE(dataLength, 2); 122 | } else if (payloadLength === 127) { 123 | target[2] = target[3] = 0; 124 | target.writeUIntBE(dataLength, 4, 6); 125 | } 126 | 127 | if (!options.mask) return [target, data]; 128 | 129 | target[1] |= 0x80; 130 | target[offset - 4] = mask[0]; 131 | target[offset - 3] = mask[1]; 132 | target[offset - 2] = mask[2]; 133 | target[offset - 1] = mask[3]; 134 | 135 | if (skipMasking) return [target, data]; 136 | 137 | if (merge) { 138 | applyMask(data, mask, target, offset, dataLength); 139 | return [target]; 140 | } 141 | 142 | applyMask(data, mask, data, 0, dataLength); 143 | return [target, data]; 144 | } 145 | 146 | /** 147 | * Sends a close message to the other peer. 148 | * 149 | * @param {Number} [code] The status code component of the body 150 | * @param {(String|Buffer)} [data] The message component of the body 151 | * @param {Boolean} [mask=false] Specifies whether or not to mask the message 152 | * @param {Function} [cb] Callback 153 | * @public 154 | */ 155 | close(code, data, mask, cb) { 156 | let buf; 157 | 158 | if (code === undefined) { 159 | buf = EMPTY_BUFFER; 160 | } else if (typeof code !== 'number' || !isValidStatusCode(code)) { 161 | throw new TypeError('First argument must be a valid error code number'); 162 | } else if (data === undefined || !data.length) { 163 | buf = Buffer.allocUnsafe(2); 164 | buf.writeUInt16BE(code, 0); 165 | } else { 166 | const length = Buffer.byteLength(data); 167 | 168 | if (length > 123) { 169 | throw new RangeError('The message must not be greater than 123 bytes'); 170 | } 171 | 172 | buf = Buffer.allocUnsafe(2 + length); 173 | buf.writeUInt16BE(code, 0); 174 | 175 | if (typeof data === 'string') { 176 | buf.write(data, 2); 177 | } else { 178 | buf.set(data, 2); 179 | } 180 | } 181 | 182 | const options = { 183 | [kByteLength]: buf.length, 184 | fin: true, 185 | generateMask: this._generateMask, 186 | mask, 187 | maskBuffer: this._maskBuffer, 188 | opcode: 0x08, 189 | readOnly: false, 190 | rsv1: false 191 | }; 192 | 193 | if (this._deflating) { 194 | this.enqueue([this.dispatch, buf, false, options, cb]); 195 | } else { 196 | this.sendFrame(Sender.frame(buf, options), cb); 197 | } 198 | } 199 | 200 | /** 201 | * Sends a ping message to the other peer. 202 | * 203 | * @param {*} data The message to send 204 | * @param {Boolean} [mask=false] Specifies whether or not to mask `data` 205 | * @param {Function} [cb] Callback 206 | * @public 207 | */ 208 | ping(data, mask, cb) { 209 | let byteLength; 210 | let readOnly; 211 | 212 | if (typeof data === 'string') { 213 | byteLength = Buffer.byteLength(data); 214 | readOnly = false; 215 | } else { 216 | data = toBuffer(data); 217 | byteLength = data.length; 218 | readOnly = toBuffer.readOnly; 219 | } 220 | 221 | if (byteLength > 125) { 222 | throw new RangeError('The data size must not be greater than 125 bytes'); 223 | } 224 | 225 | const options = { 226 | [kByteLength]: byteLength, 227 | fin: true, 228 | generateMask: this._generateMask, 229 | mask, 230 | maskBuffer: this._maskBuffer, 231 | opcode: 0x09, 232 | readOnly, 233 | rsv1: false 234 | }; 235 | 236 | if (this._deflating) { 237 | this.enqueue([this.dispatch, data, false, options, cb]); 238 | } else { 239 | this.sendFrame(Sender.frame(data, options), cb); 240 | } 241 | } 242 | 243 | /** 244 | * Sends a pong message to the other peer. 245 | * 246 | * @param {*} data The message to send 247 | * @param {Boolean} [mask=false] Specifies whether or not to mask `data` 248 | * @param {Function} [cb] Callback 249 | * @public 250 | */ 251 | pong(data, mask, cb) { 252 | let byteLength; 253 | let readOnly; 254 | 255 | if (typeof data === 'string') { 256 | byteLength = Buffer.byteLength(data); 257 | readOnly = false; 258 | } else { 259 | data = toBuffer(data); 260 | byteLength = data.length; 261 | readOnly = toBuffer.readOnly; 262 | } 263 | 264 | if (byteLength > 125) { 265 | throw new RangeError('The data size must not be greater than 125 bytes'); 266 | } 267 | 268 | const options = { 269 | [kByteLength]: byteLength, 270 | fin: true, 271 | generateMask: this._generateMask, 272 | mask, 273 | maskBuffer: this._maskBuffer, 274 | opcode: 0x0a, 275 | readOnly, 276 | rsv1: false 277 | }; 278 | 279 | if (this._deflating) { 280 | this.enqueue([this.dispatch, data, false, options, cb]); 281 | } else { 282 | this.sendFrame(Sender.frame(data, options), cb); 283 | } 284 | } 285 | 286 | /** 287 | * Sends a data message to the other peer. 288 | * 289 | * @param {*} data The message to send 290 | * @param {Object} options Options object 291 | * @param {Boolean} [options.binary=false] Specifies whether `data` is binary 292 | * or text 293 | * @param {Boolean} [options.compress=false] Specifies whether or not to 294 | * compress `data` 295 | * @param {Boolean} [options.fin=false] Specifies whether the fragment is the 296 | * last one 297 | * @param {Boolean} [options.mask=false] Specifies whether or not to mask 298 | * `data` 299 | * @param {Function} [cb] Callback 300 | * @public 301 | */ 302 | send(data, options, cb) { 303 | const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName]; 304 | let opcode = options.binary ? 2 : 1; 305 | let rsv1 = options.compress; 306 | 307 | let byteLength; 308 | let readOnly; 309 | 310 | if (typeof data === 'string') { 311 | byteLength = Buffer.byteLength(data); 312 | readOnly = false; 313 | } else { 314 | data = toBuffer(data); 315 | byteLength = data.length; 316 | readOnly = toBuffer.readOnly; 317 | } 318 | 319 | if (this._firstFragment) { 320 | this._firstFragment = false; 321 | if ( 322 | rsv1 && 323 | perMessageDeflate && 324 | perMessageDeflate.params[ 325 | perMessageDeflate._isServer 326 | ? 'server_no_context_takeover' 327 | : 'client_no_context_takeover' 328 | ] 329 | ) { 330 | rsv1 = byteLength >= perMessageDeflate._threshold; 331 | } 332 | this._compress = rsv1; 333 | } else { 334 | rsv1 = false; 335 | opcode = 0; 336 | } 337 | 338 | if (options.fin) this._firstFragment = true; 339 | 340 | if (perMessageDeflate) { 341 | const opts = { 342 | [kByteLength]: byteLength, 343 | fin: options.fin, 344 | generateMask: this._generateMask, 345 | mask: options.mask, 346 | maskBuffer: this._maskBuffer, 347 | opcode, 348 | readOnly, 349 | rsv1 350 | }; 351 | 352 | if (this._deflating) { 353 | this.enqueue([this.dispatch, data, this._compress, opts, cb]); 354 | } else { 355 | this.dispatch(data, this._compress, opts, cb); 356 | } 357 | } else { 358 | this.sendFrame( 359 | Sender.frame(data, { 360 | [kByteLength]: byteLength, 361 | fin: options.fin, 362 | generateMask: this._generateMask, 363 | mask: options.mask, 364 | maskBuffer: this._maskBuffer, 365 | opcode, 366 | readOnly, 367 | rsv1: false 368 | }), 369 | cb 370 | ); 371 | } 372 | } 373 | 374 | /** 375 | * Dispatches a message. 376 | * 377 | * @param {(Buffer|String)} data The message to send 378 | * @param {Boolean} [compress=false] Specifies whether or not to compress 379 | * `data` 380 | * @param {Object} options Options object 381 | * @param {Boolean} [options.fin=false] Specifies whether or not to set the 382 | * FIN bit 383 | * @param {Function} [options.generateMask] The function used to generate the 384 | * masking key 385 | * @param {Boolean} [options.mask=false] Specifies whether or not to mask 386 | * `data` 387 | * @param {Buffer} [options.maskBuffer] The buffer used to store the masking 388 | * key 389 | * @param {Number} options.opcode The opcode 390 | * @param {Boolean} [options.readOnly=false] Specifies whether `data` can be 391 | * modified 392 | * @param {Boolean} [options.rsv1=false] Specifies whether or not to set the 393 | * RSV1 bit 394 | * @param {Function} [cb] Callback 395 | * @private 396 | */ 397 | dispatch(data, compress, options, cb) { 398 | if (!compress) { 399 | this.sendFrame(Sender.frame(data, options), cb); 400 | return; 401 | } 402 | 403 | const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName]; 404 | 405 | this._bufferedBytes += options[kByteLength]; 406 | this._deflating = true; 407 | perMessageDeflate.compress(data, options.fin, (_, buf) => { 408 | if (this._socket.destroyed) { 409 | const err = new Error( 410 | 'The socket was closed while data was being compressed' 411 | ); 412 | 413 | if (typeof cb === 'function') cb(err); 414 | 415 | for (let i = 0; i < this._queue.length; i++) { 416 | const params = this._queue[i]; 417 | const callback = params[params.length - 1]; 418 | 419 | if (typeof callback === 'function') callback(err); 420 | } 421 | 422 | return; 423 | } 424 | 425 | this._bufferedBytes -= options[kByteLength]; 426 | this._deflating = false; 427 | options.readOnly = false; 428 | this.sendFrame(Sender.frame(buf, options), cb); 429 | this.dequeue(); 430 | }); 431 | } 432 | 433 | /** 434 | * Executes queued send operations. 435 | * 436 | * @private 437 | */ 438 | dequeue() { 439 | while (!this._deflating && this._queue.length) { 440 | const params = this._queue.shift(); 441 | 442 | this._bufferedBytes -= params[3][kByteLength]; 443 | Reflect.apply(params[0], this, params.slice(1)); 444 | } 445 | } 446 | 447 | /** 448 | * Enqueues a send operation. 449 | * 450 | * @param {Array} params Send operation parameters. 451 | * @private 452 | */ 453 | enqueue(params) { 454 | this._bufferedBytes += params[3][kByteLength]; 455 | this._queue.push(params); 456 | } 457 | 458 | /** 459 | * Sends a frame. 460 | * 461 | * @param {Buffer[]} list The frame to send 462 | * @param {Function} [cb] Callback 463 | * @private 464 | */ 465 | sendFrame(list, cb) { 466 | if (list.length === 2) { 467 | this._socket.cork(); 468 | this._socket.write(list[0]); 469 | this._socket.write(list[1], cb); 470 | this._socket.uncork(); 471 | } else { 472 | this._socket.write(list[0], cb); 473 | } 474 | } 475 | } 476 | 477 | module.exports = Sender; 478 | -------------------------------------------------------------------------------- /src/lib/permessage-deflate.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const zlib = require('zlib'); 4 | 5 | const bufferUtil = require('./buffer-util'); 6 | const Limiter = require('./limiter'); 7 | const { kStatusCode } = require('./constants'); 8 | 9 | const FastBuffer = Buffer[Symbol.species]; 10 | const TRAILER = Buffer.from([0x00, 0x00, 0xff, 0xff]); 11 | const kPerMessageDeflate = Symbol('permessage-deflate'); 12 | const kTotalLength = Symbol('total-length'); 13 | const kCallback = Symbol('callback'); 14 | const kBuffers = Symbol('buffers'); 15 | const kError = Symbol('error'); 16 | 17 | // 18 | // We limit zlib concurrency, which prevents severe memory fragmentation 19 | // as documented in https://github.com/nodejs/node/issues/8871#issuecomment-250915913 20 | // and https://github.com/websockets/ws/issues/1202 21 | // 22 | // Intentionally global; it's the global thread pool that's an issue. 23 | // 24 | let zlibLimiter; 25 | 26 | /** 27 | * permessage-deflate implementation. 28 | */ 29 | class PerMessageDeflate { 30 | /** 31 | * Creates a PerMessageDeflate instance. 32 | * 33 | * @param {Object} [options] Configuration options 34 | * @param {(Boolean|Number)} [options.clientMaxWindowBits] Advertise support 35 | * for, or request, a custom client window size 36 | * @param {Boolean} [options.clientNoContextTakeover=false] Advertise/ 37 | * acknowledge disabling of client context takeover 38 | * @param {Number} [options.concurrencyLimit=10] The number of concurrent 39 | * calls to zlib 40 | * @param {(Boolean|Number)} [options.serverMaxWindowBits] Request/confirm the 41 | * use of a custom server window size 42 | * @param {Boolean} [options.serverNoContextTakeover=false] Request/accept 43 | * disabling of server context takeover 44 | * @param {Number} [options.threshold=1024] Size (in bytes) below which 45 | * messages should not be compressed if context takeover is disabled 46 | * @param {Object} [options.zlibDeflateOptions] Options to pass to zlib on 47 | * deflate 48 | * @param {Object} [options.zlibInflateOptions] Options to pass to zlib on 49 | * inflate 50 | * @param {Boolean} [isServer=false] Create the instance in either server or 51 | * client mode 52 | * @param {Number} [maxPayload=0] The maximum allowed message length 53 | */ 54 | constructor(options, isServer, maxPayload) { 55 | this._maxPayload = maxPayload | 0; 56 | this._options = options || {}; 57 | this._threshold = 58 | this._options.threshold !== undefined ? this._options.threshold : 1024; 59 | this._isServer = !!isServer; 60 | this._deflate = null; 61 | this._inflate = null; 62 | 63 | this.params = null; 64 | 65 | if (!zlibLimiter) { 66 | const concurrency = 67 | this._options.concurrencyLimit !== undefined 68 | ? this._options.concurrencyLimit 69 | : 10; 70 | zlibLimiter = new Limiter(concurrency); 71 | } 72 | } 73 | 74 | /** 75 | * @type {String} 76 | */ 77 | static get extensionName() { 78 | return 'permessage-deflate'; 79 | } 80 | 81 | /** 82 | * Create an extension negotiation offer. 83 | * 84 | * @return {Object} Extension parameters 85 | * @public 86 | */ 87 | offer() { 88 | const params = {}; 89 | 90 | if (this._options.serverNoContextTakeover) { 91 | params.server_no_context_takeover = true; 92 | } 93 | if (this._options.clientNoContextTakeover) { 94 | params.client_no_context_takeover = true; 95 | } 96 | if (this._options.serverMaxWindowBits) { 97 | params.server_max_window_bits = this._options.serverMaxWindowBits; 98 | } 99 | if (this._options.clientMaxWindowBits) { 100 | params.client_max_window_bits = this._options.clientMaxWindowBits; 101 | } else if (this._options.clientMaxWindowBits == null) { 102 | params.client_max_window_bits = true; 103 | } 104 | 105 | return params; 106 | } 107 | 108 | /** 109 | * Accept an extension negotiation offer/response. 110 | * 111 | * @param {Array} configurations The extension negotiation offers/reponse 112 | * @return {Object} Accepted configuration 113 | * @public 114 | */ 115 | accept(configurations) { 116 | configurations = this.normalizeParams(configurations); 117 | 118 | this.params = this._isServer 119 | ? this.acceptAsServer(configurations) 120 | : this.acceptAsClient(configurations); 121 | 122 | return this.params; 123 | } 124 | 125 | /** 126 | * Releases all resources used by the extension. 127 | * 128 | * @public 129 | */ 130 | cleanup() { 131 | if (this._inflate) { 132 | this._inflate.close(); 133 | this._inflate = null; 134 | } 135 | 136 | if (this._deflate) { 137 | const callback = this._deflate[kCallback]; 138 | 139 | this._deflate.close(); 140 | this._deflate = null; 141 | 142 | if (callback) { 143 | callback( 144 | new Error( 145 | 'The deflate stream was closed while data was being processed' 146 | ) 147 | ); 148 | } 149 | } 150 | } 151 | 152 | /** 153 | * Accept an extension negotiation offer. 154 | * 155 | * @param {Array} offers The extension negotiation offers 156 | * @return {Object} Accepted configuration 157 | * @private 158 | */ 159 | acceptAsServer(offers) { 160 | const opts = this._options; 161 | const accepted = offers.find((params) => { 162 | if ( 163 | (opts.serverNoContextTakeover === false && 164 | params.server_no_context_takeover) || 165 | (params.server_max_window_bits && 166 | (opts.serverMaxWindowBits === false || 167 | (typeof opts.serverMaxWindowBits === 'number' && 168 | opts.serverMaxWindowBits > params.server_max_window_bits))) || 169 | (typeof opts.clientMaxWindowBits === 'number' && 170 | !params.client_max_window_bits) 171 | ) { 172 | return false; 173 | } 174 | 175 | return true; 176 | }); 177 | 178 | if (!accepted) { 179 | throw new Error('None of the extension offers can be accepted'); 180 | } 181 | 182 | if (opts.serverNoContextTakeover) { 183 | accepted.server_no_context_takeover = true; 184 | } 185 | if (opts.clientNoContextTakeover) { 186 | accepted.client_no_context_takeover = true; 187 | } 188 | if (typeof opts.serverMaxWindowBits === 'number') { 189 | accepted.server_max_window_bits = opts.serverMaxWindowBits; 190 | } 191 | if (typeof opts.clientMaxWindowBits === 'number') { 192 | accepted.client_max_window_bits = opts.clientMaxWindowBits; 193 | } else if ( 194 | accepted.client_max_window_bits === true || 195 | opts.clientMaxWindowBits === false 196 | ) { 197 | delete accepted.client_max_window_bits; 198 | } 199 | 200 | return accepted; 201 | } 202 | 203 | /** 204 | * Accept the extension negotiation response. 205 | * 206 | * @param {Array} response The extension negotiation response 207 | * @return {Object} Accepted configuration 208 | * @private 209 | */ 210 | acceptAsClient(response) { 211 | const params = response[0]; 212 | 213 | if ( 214 | this._options.clientNoContextTakeover === false && 215 | params.client_no_context_takeover 216 | ) { 217 | throw new Error('Unexpected parameter "client_no_context_takeover"'); 218 | } 219 | 220 | if (!params.client_max_window_bits) { 221 | if (typeof this._options.clientMaxWindowBits === 'number') { 222 | params.client_max_window_bits = this._options.clientMaxWindowBits; 223 | } 224 | } else if ( 225 | this._options.clientMaxWindowBits === false || 226 | (typeof this._options.clientMaxWindowBits === 'number' && 227 | params.client_max_window_bits > this._options.clientMaxWindowBits) 228 | ) { 229 | throw new Error( 230 | 'Unexpected or invalid parameter "client_max_window_bits"' 231 | ); 232 | } 233 | 234 | return params; 235 | } 236 | 237 | /** 238 | * Normalize parameters. 239 | * 240 | * @param {Array} configurations The extension negotiation offers/reponse 241 | * @return {Array} The offers/response with normalized parameters 242 | * @private 243 | */ 244 | normalizeParams(configurations) { 245 | configurations.forEach((params) => { 246 | Object.keys(params).forEach((key) => { 247 | let value = params[key]; 248 | 249 | if (value.length > 1) { 250 | throw new Error(`Parameter "${key}" must have only a single value`); 251 | } 252 | 253 | value = value[0]; 254 | 255 | if (key === 'client_max_window_bits') { 256 | if (value !== true) { 257 | const num = +value; 258 | if (!Number.isInteger(num) || num < 8 || num > 15) { 259 | throw new TypeError( 260 | `Invalid value for parameter "${key}": ${value}` 261 | ); 262 | } 263 | value = num; 264 | } else if (!this._isServer) { 265 | throw new TypeError( 266 | `Invalid value for parameter "${key}": ${value}` 267 | ); 268 | } 269 | } else if (key === 'server_max_window_bits') { 270 | const num = +value; 271 | if (!Number.isInteger(num) || num < 8 || num > 15) { 272 | throw new TypeError( 273 | `Invalid value for parameter "${key}": ${value}` 274 | ); 275 | } 276 | value = num; 277 | } else if ( 278 | key === 'client_no_context_takeover' || 279 | key === 'server_no_context_takeover' 280 | ) { 281 | if (value !== true) { 282 | throw new TypeError( 283 | `Invalid value for parameter "${key}": ${value}` 284 | ); 285 | } 286 | } else { 287 | throw new Error(`Unknown parameter "${key}"`); 288 | } 289 | 290 | params[key] = value; 291 | }); 292 | }); 293 | 294 | return configurations; 295 | } 296 | 297 | /** 298 | * Decompress data. Concurrency limited. 299 | * 300 | * @param {Buffer} data Compressed data 301 | * @param {Boolean} fin Specifies whether or not this is the last fragment 302 | * @param {Function} callback Callback 303 | * @public 304 | */ 305 | decompress(data, fin, callback) { 306 | zlibLimiter.add((done) => { 307 | this._decompress(data, fin, (err, result) => { 308 | done(); 309 | callback(err, result); 310 | }); 311 | }); 312 | } 313 | 314 | /** 315 | * Compress data. Concurrency limited. 316 | * 317 | * @param {(Buffer|String)} data Data to compress 318 | * @param {Boolean} fin Specifies whether or not this is the last fragment 319 | * @param {Function} callback Callback 320 | * @public 321 | */ 322 | compress(data, fin, callback) { 323 | zlibLimiter.add((done) => { 324 | this._compress(data, fin, (err, result) => { 325 | done(); 326 | callback(err, result); 327 | }); 328 | }); 329 | } 330 | 331 | /** 332 | * Decompress data. 333 | * 334 | * @param {Buffer} data Compressed data 335 | * @param {Boolean} fin Specifies whether or not this is the last fragment 336 | * @param {Function} callback Callback 337 | * @private 338 | */ 339 | _decompress(data, fin, callback) { 340 | const endpoint = this._isServer ? 'client' : 'server'; 341 | 342 | if (!this._inflate) { 343 | const key = `${endpoint}_max_window_bits`; 344 | const windowBits = 345 | typeof this.params[key] !== 'number' 346 | ? zlib.Z_DEFAULT_WINDOWBITS 347 | : this.params[key]; 348 | 349 | this._inflate = zlib.createInflateRaw({ 350 | ...this._options.zlibInflateOptions, 351 | windowBits 352 | }); 353 | this._inflate[kPerMessageDeflate] = this; 354 | this._inflate[kTotalLength] = 0; 355 | this._inflate[kBuffers] = []; 356 | this._inflate.on('error', inflateOnError); 357 | this._inflate.on('data', inflateOnData); 358 | } 359 | 360 | this._inflate[kCallback] = callback; 361 | 362 | this._inflate.write(data); 363 | if (fin) this._inflate.write(TRAILER); 364 | 365 | this._inflate.flush(() => { 366 | const err = this._inflate[kError]; 367 | 368 | if (err) { 369 | this._inflate.close(); 370 | this._inflate = null; 371 | callback(err); 372 | return; 373 | } 374 | 375 | const data = bufferUtil.concat( 376 | this._inflate[kBuffers], 377 | this._inflate[kTotalLength] 378 | ); 379 | 380 | if (this._inflate._readableState.endEmitted) { 381 | this._inflate.close(); 382 | this._inflate = null; 383 | } else { 384 | this._inflate[kTotalLength] = 0; 385 | this._inflate[kBuffers] = []; 386 | 387 | if (fin && this.params[`${endpoint}_no_context_takeover`]) { 388 | this._inflate.reset(); 389 | } 390 | } 391 | 392 | callback(null, data); 393 | }); 394 | } 395 | 396 | /** 397 | * Compress data. 398 | * 399 | * @param {(Buffer|String)} data Data to compress 400 | * @param {Boolean} fin Specifies whether or not this is the last fragment 401 | * @param {Function} callback Callback 402 | * @private 403 | */ 404 | _compress(data, fin, callback) { 405 | const endpoint = this._isServer ? 'server' : 'client'; 406 | 407 | if (!this._deflate) { 408 | const key = `${endpoint}_max_window_bits`; 409 | const windowBits = 410 | typeof this.params[key] !== 'number' 411 | ? zlib.Z_DEFAULT_WINDOWBITS 412 | : this.params[key]; 413 | 414 | this._deflate = zlib.createDeflateRaw({ 415 | ...this._options.zlibDeflateOptions, 416 | windowBits 417 | }); 418 | 419 | this._deflate[kTotalLength] = 0; 420 | this._deflate[kBuffers] = []; 421 | 422 | this._deflate.on('data', deflateOnData); 423 | } 424 | 425 | this._deflate[kCallback] = callback; 426 | 427 | this._deflate.write(data); 428 | this._deflate.flush(zlib.Z_SYNC_FLUSH, () => { 429 | if (!this._deflate) { 430 | // 431 | // The deflate stream was closed while data was being processed. 432 | // 433 | return; 434 | } 435 | 436 | let data = bufferUtil.concat( 437 | this._deflate[kBuffers], 438 | this._deflate[kTotalLength] 439 | ); 440 | 441 | if (fin) { 442 | data = new FastBuffer(data.buffer, data.byteOffset, data.length - 4); 443 | } 444 | 445 | // 446 | // Ensure that the callback will not be called again in 447 | // `PerMessageDeflate#cleanup()`. 448 | // 449 | this._deflate[kCallback] = null; 450 | 451 | this._deflate[kTotalLength] = 0; 452 | this._deflate[kBuffers] = []; 453 | 454 | if (fin && this.params[`${endpoint}_no_context_takeover`]) { 455 | this._deflate.reset(); 456 | } 457 | 458 | callback(null, data); 459 | }); 460 | } 461 | } 462 | 463 | module.exports = PerMessageDeflate; 464 | 465 | /** 466 | * The listener of the `zlib.DeflateRaw` stream `'data'` event. 467 | * 468 | * @param {Buffer} chunk A chunk of data 469 | * @private 470 | */ 471 | function deflateOnData(chunk) { 472 | this[kBuffers].push(chunk); 473 | this[kTotalLength] += chunk.length; 474 | } 475 | 476 | /** 477 | * The listener of the `zlib.InflateRaw` stream `'data'` event. 478 | * 479 | * @param {Buffer} chunk A chunk of data 480 | * @private 481 | */ 482 | function inflateOnData(chunk) { 483 | this[kTotalLength] += chunk.length; 484 | 485 | if ( 486 | this[kPerMessageDeflate]._maxPayload < 1 || 487 | this[kTotalLength] <= this[kPerMessageDeflate]._maxPayload 488 | ) { 489 | this[kBuffers].push(chunk); 490 | return; 491 | } 492 | 493 | this[kError] = new RangeError('Max payload size exceeded'); 494 | this[kError].code = 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH'; 495 | this[kError][kStatusCode] = 1009; 496 | this.removeListener('data', inflateOnData); 497 | this.reset(); 498 | } 499 | 500 | /** 501 | * The listener of the `zlib.InflateRaw` stream `'error'` event. 502 | * 503 | * @param {Error} err The emitted error 504 | * @private 505 | */ 506 | function inflateOnError(err) { 507 | // 508 | // There is no need to call `Zlib#close()` as the handle is automatically 509 | // closed when an error is emitted. 510 | // 511 | this[kPerMessageDeflate]._inflate = null; 512 | err[kStatusCode] = 1007; 513 | this[kCallback](err); 514 | } 515 | -------------------------------------------------------------------------------- /src/oneBot11/api.js: -------------------------------------------------------------------------------- 1 | const {Data} = require("../main/core"); 2 | const {Text, createOneBot} = require("./message"); 3 | const Event = require("./event"); 4 | const {QQNtAPI} = require("../qqnt/QQNtAPI"); 5 | const {Log} = require("../logger"); 6 | 7 | const OneBotOK = { status: "ok", retcode: 0 }; 8 | 9 | function OneBotFail(code, data = ""){ 10 | return { status: 'fail', retcode: code, data: data }; 11 | } 12 | 13 | class BaseApi{ 14 | constructor(url){ 15 | this.url = url; 16 | } 17 | async handle(postData){ 18 | return OneBotOK; 19 | } 20 | } 21 | 22 | 23 | class NtCall extends BaseApi{ 24 | 25 | constructor(){ super("__ntCall") } 26 | 27 | async handle(postData){ 28 | return { 29 | code: 200, 30 | msg: "OK", 31 | data: await QQNtAPI.ntCall(postData['eventName'], postData['cmdName'], postData['args'], postData['webContentsId'] || '2') 32 | }; 33 | } 34 | } 35 | 36 | class NtCallAsync extends BaseApi{ 37 | 38 | constructor(){ super("__ntCallAsync") } 39 | 40 | async handle(postData){ 41 | return { 42 | code: 200, 43 | msg: "OK", 44 | data: await QQNtAPI.ntCallAsync( 45 | postData['eventName'], 46 | postData['cmdName'], 47 | postData['args'], 48 | postData['callBackCmdName'], 49 | () => {return true}, 50 | false, 51 | postData['webContentsId'] || '2' 52 | ) 53 | }; 54 | } 55 | } 56 | 57 | /** 58 | * 获取好友列表
59 | * result: 60 | * user_id: QQ号, 61 | * nickname: 昵称, 62 | * remark: 备注 63 | */ 64 | class GetFriendList extends BaseApi { 65 | constructor(){ super("get_friend_list") } 66 | 67 | async handle(postData){ 68 | return { 69 | status: 'ok', 70 | retcode: 0, 71 | data: Object.values(Data.friends).map(friend => { 72 | return { 73 | user_id: friend.uin, 74 | nickname: friend.nick, 75 | remark: friend.remark 76 | } 77 | }) 78 | }; 79 | } 80 | } 81 | 82 | class SendLike extends BaseApi{ 83 | 84 | constructor(){ super("send_like") } 85 | 86 | async handle(postData){ 87 | let user = Data.getInfoByQQ(postData.user_id); 88 | if(user == null) return OneBotFail(404, "好友不存在"); 89 | 90 | let result = (await QQNtAPI.ntCall( 91 | "ns-ntApi", 92 | "nodeIKernelProfileLikeService/setBuddyProfileLike", 93 | [{ 94 | "doLikeUserInfo":{ 95 | "friendUid": user.uid, 96 | "sourceId": 71, 97 | "doLikeCount": postData.times || 1, 98 | "doLikeTollCount": 0 99 | } 100 | }, null] 101 | )) 102 | 103 | if(result.succCounts > 0) return OneBotOK; else return OneBotFail(500, "今日点赞次数已达上限"); 104 | } 105 | } 106 | 107 | 108 | /** 109 | * 获取群信息 110 | * { "group_id": 123456 } 111 | * 112 | * result: 113 | * { 114 | * code: 200, 115 | * msg: "OK", 116 | * data: { 117 | * group_id: 群号, 118 | * group_name: 群名称, 119 | * member_count: 成员数, 120 | * max_member_count: 最大成员数(群容量) 121 | * } 122 | * } 123 | */ 124 | class GetGroupInfo extends BaseApi{ 125 | 126 | constructor(){ super("get_group_info") } 127 | 128 | async handle(postData){ 129 | const group = Data.groups[postData.group_id] 130 | 131 | return { 132 | status: 'ok', 133 | retcode: 0, 134 | data: { 135 | 'group_id': group.groupCode, 136 | 'group_name': group.groupName, 137 | 'member_count': group.memberCount, 138 | 'max_member_count': group.maxMember, 139 | } 140 | }; 141 | } 142 | } 143 | 144 | /** 145 | * 获取群成员列表 146 | * { "group_id": 123456 } 147 | * 148 | * result: 149 | * { 150 | * code: 200, 151 | * msg: "OK", 152 | * data: [ 153 | * 154 | * ] 155 | */ 156 | class GetGroupMemberList extends BaseApi{ 157 | 158 | constructor(){ super("get_group_member_list") } 159 | 160 | async handle(postData){ 161 | let members = await Data.getGroupMemberList(postData.group_id, (postData?.no_cache || false)); 162 | return { 163 | status: 'ok', 164 | retcode: 0, 165 | data: members.map((member) => { return { 166 | group_id: postData.group_id,// 群号 167 | user_id: member.uin, // QQ 号 168 | nickname: member.nick, // 昵称 169 | card: member.cardName, // 群名片/备注 170 | role: member.role == 4 ? 'owner' : (member.role == 3 ? 'admin' : (member.role == 2 ? 'member' : 'unknown')), // 角色,owner 或 admin 或 member 171 | }}) 172 | }; 173 | } 174 | } 175 | 176 | class GetGroupMemberInfo extends BaseApi{ 177 | constructor(){ super("get_group_member_info") } 178 | 179 | async handle(postData){ 180 | let member = await Data.getGroupMemberByQQ(postData.group_id, postData.user_id, false); 181 | if(!member) return OneBotFail(404, "用户在群聊中不存在"); 182 | 183 | let info = await Data.getGroupMemberInfo(postData.group_id, postData.user_id, false); 184 | member.info = info 185 | 186 | return { 187 | status: 'ok', 188 | retcode: 0, 189 | data: { 190 | group_id: postData.group_id,// 群号 191 | user_id: member.uin, // QQ 号 192 | nickname: member.nick, // 昵称 193 | card: member.cardName, // 群名片/备注 194 | level: member.memberRealLevel, // 群等级 195 | role: member.role == 4 ? 'owner' : (member.role == 3 ? 'admin' : (member.role == 2 ? 'member' : 'unknown')), // 角色,owner 或 admin 或 member 196 | 197 | sex: info?.sex == 1 ? "male" : (info?.sex == 2 ? "female" : "unknown"), 198 | age: new Date().getFullYear() - info?.birthday_year, 199 | area: `${info?.country} ${info?.province} ${info?.city}`, 200 | 201 | raw: member 202 | } 203 | } 204 | } 205 | } 206 | 207 | /** 208 | * 获取群列表 209 | * result: 210 | * { 211 | * code: 200, 212 | * msg: "OK", 213 | * data: [ 214 | * { 215 | * group_id: 群号, 216 | * group_name: 群名称, 217 | * member_count: 成员数, 218 | * max_member_count: 最大成员数(群容量) 219 | * }, 220 | * ... 221 | * ] 222 | * } 223 | */ 224 | class getGroupList extends BaseApi{ 225 | 226 | constructor(){ super("get_group_list") } 227 | 228 | async handle(postData){ 229 | return { 230 | status: 'ok', 231 | retcode: 0, 232 | data: Object.values(Data.groups).map(group => { 233 | return{ 234 | 'group_id': group.groupCode, 235 | 'group_name': group.groupName, 236 | 'member_count': group.memberCount, 237 | 'max_member_count': group.maxMember, 238 | } 239 | }) 240 | }; 241 | } 242 | } 243 | 244 | class getGroupMsgMask extends BaseApi{ 245 | 246 | constructor(){ super("get_group_msg_mask") } 247 | 248 | async handle(postData){ 249 | const type = { 250 | 1: "接收并提醒", 251 | 2: "收入群助手且不提醒", 252 | 3: "屏蔽消息", 253 | 4: "接受消息但不提醒", 254 | } 255 | const groups = Data.groups 256 | 257 | return { 258 | status: 'ok', 259 | retcode: 0, 260 | data: 261 | (await QQNtAPI.ntCallAsync("ns-ntApi", "nodeIKernelGroupService/getGroupMsgMask", [null, null], 262 | "nodeIKernelGroupListener/onGroupsMsgMaskResult", 263 | () => { return true }, 264 | false, 265 | '3' 266 | )).payload.groupsMsgMask.map(group => { 267 | return { 268 | 'group_id': group.groupCode, 269 | 'group_name': groups[group.groupCode].groupName, 270 | 'msg_mask': group.msgMask, 271 | 'msg_type': type[group.msgMask], 272 | } 273 | }) 274 | }; 275 | } 276 | } 277 | 278 | class KickMember extends BaseApi{ 279 | 280 | constructor(){ super("set_group_kick") } 281 | 282 | async handle(postData){ 283 | let group = Data.getGroupById(postData.group_id); 284 | if(group == null) return OneBotFail(404, '群不存在'); 285 | 286 | let user = await Data.getGroupMemberByQQ(postData.group_id, postData.user_id); 287 | if(user == null) return OneBotFail(404, "群成员不存在"); 288 | 289 | let result = (await QQNtAPI.ntCall( 290 | "ns-ntApi", 291 | "nodeIKernelGroupService/kickMember", 292 | [{ 293 | "groupCode": group.groupCode, 294 | "kickUids":[ user.uid ], 295 | "refuseForever": !!postData.reject_add_request, 296 | "kickReason": "" 297 | }, null] 298 | )) 299 | 300 | if(result.errCode == 0) return OneBotOK; else return OneBotFail(result.errCode); 301 | } 302 | } 303 | 304 | class setGroupAdmin extends BaseApi{ 305 | 306 | constructor(){ super("set_group_admin") } 307 | 308 | async handle(postData){ 309 | let group = Data.getGroupById(postData.group_id); 310 | if(group == null) return OneBotFail(404, "群不存在"); 311 | 312 | let user = await Data.getGroupMemberByQQ(postData.group_id, postData.user_id); 313 | if(user == null) return OneBotFail(404, "群成员不存在"); 314 | 315 | let result = (await QQNtAPI.ntCall( 316 | "ns-ntApi", 317 | "nodeIKernelGroupService/modifyMemberRole", 318 | [{ 319 | "groupCode": group.groupCode, 320 | "uid": user.uid, 321 | "role": !!postData.enable ? 3 : 2, 322 | }, null] 323 | )) 324 | 325 | if(result.result == 0) return OneBotOK; else return OneBotFail(result.result, result.errMsg); 326 | } 327 | } 328 | 329 | class setGroupBan extends BaseApi{ 330 | 331 | constructor(){ super("set_group_ban") } 332 | 333 | async handle(postData){ 334 | let group = Data.getGroupById(postData.group_id); 335 | if(group == null) return OneBotFail(404, '群不存在'); 336 | 337 | let user = await Data.getGroupMemberByQQ(postData.group_id, postData.user_id); 338 | if(user == null) return OneBotFail(404, "群成员不存在"); 339 | 340 | let result = (await QQNtAPI.ntCall( 341 | "ns-ntApi", 342 | "nodeIKernelGroupService/setMemberShutUp", 343 | [{ 344 | groupCode: group.groupCode, 345 | memList: [{ uid: user.uid, timeStamp: postData.duration | 0 }] 346 | }, null] 347 | )) 348 | 349 | if(result.result == 0) return OneBotOK; else return OneBotFail(500, result); 350 | } 351 | } 352 | 353 | class setGroupWholeBan extends BaseApi{ 354 | 355 | constructor(){ super("set_group_whole_ban") } 356 | 357 | async handle(postData){ 358 | let group = Data.getGroupById(postData.group_id); 359 | if(group == null) return OneBotFail(404, '群不存在'); 360 | 361 | 362 | let result = (await QQNtAPI.ntCall( 363 | "ns-ntApi", 364 | "nodeIKernelGroupService/setGroupShutUp", 365 | [{ 366 | "groupCode": group.groupCode, 367 | "shutUp": !!postData.enable 368 | }, null] 369 | )) 370 | 371 | if(result.result == 0) return OneBotOK; else return OneBotFail(500, result); 372 | } 373 | } 374 | 375 | class setGroupName extends BaseApi{ 376 | 377 | constructor(){ super("set_group_name") } 378 | 379 | async handle(postData){ 380 | let group = Data.getGroupById(postData.group_id); 381 | if(group == null) return OneBotFail(404, '群不存在'); 382 | 383 | 384 | if(postData.group_name == null || group.group_name == ""){ 385 | return { status: 'fail', retcode: 500, data: '群名称不能为空' }; 386 | } 387 | 388 | let result = (await QQNtAPI.ntCall( 389 | "ns-ntApi", 390 | "nodeIKernelGroupService/modifyGroupName", 391 | [{ 392 | groupCode: group.groupCode, 393 | groupName: postData.group_name, 394 | }, null] 395 | )) 396 | 397 | if(result.result == 0) return OneBotOK; else return OneBotFail(500, result); 398 | } 399 | } 400 | 401 | class setGroupSpecialTitle extends BaseApi{ 402 | 403 | constructor(){ super("set_group_special_title") } 404 | 405 | async handle(postData){ 406 | let group = Data.getGroupById(postData.group_id); 407 | if(group == null) return OneBotFail(404, '群不存在'); 408 | 409 | let user = await Data.getGroupMemberByQQ(postData.group_id, postData.user_id); 410 | if(user == null) return OneBotFail(404, '群成员不存在'); 411 | 412 | // {"errCode":0,"errMsg":"success","resultList":[{"uid":"u_uMoA","result":0}]} 413 | 414 | return OneBotFail(404); 415 | } 416 | } 417 | 418 | class setGroupCard extends BaseApi{ 419 | 420 | constructor(){ super("set_group_card") } 421 | 422 | async handle(postData){ 423 | let group = Data.getGroupById(postData.group_id); 424 | if(group == null) return OneBotFail(404, '群不存在'); 425 | 426 | let user = await Data.getGroupMemberByQQ(postData.group_id, postData.user_id); 427 | if(user == null) return OneBotFail(404, '群成员不存在'); 428 | 429 | let result = (await QQNtAPI.ntCall( 430 | "ns-ntApi", 431 | "nodeIKernelGroupService/modifyMemberCardName", 432 | [{ 433 | "groupCode": group.groupCode, 434 | "uid": user.uid, 435 | "cardName": postData.card || '' 436 | }, null] 437 | )) 438 | 439 | if(result.result == 0) return OneBotOK; else return OneBotFail(result.result); 440 | } 441 | } 442 | 443 | class setGroupLeave extends BaseApi{ 444 | 445 | constructor(){ super("set_group_leave") } 446 | 447 | async handle(postData){ 448 | let group = Data.getGroupById(postData.group_id); 449 | if(group == null) return OneBotFail(404, '群不存在'); 450 | 451 | let result = (await QQNtAPI.ntCall( 452 | "ns-ntApi", 453 | "nodeIKernelGroupService/quitGroup", 454 | [{ "groupCode": group.groupCode }, null] 455 | )) 456 | 457 | if(result.errCode == 0) return OneBotOK; else return OneBotFail(result.errCode); 458 | } 459 | } 460 | 461 | class SetGroupAddRequest extends BaseApi{ 462 | 463 | constructor(){ super("set_group_add_request") } 464 | 465 | async handle(postData){ 466 | if(!('flag' in postData)) return OneBotFail(400, "Must provide 'flag'"); 467 | 468 | return { 469 | status: 'ok', 470 | retcode: 0, 471 | data: await QQNtAPI.ntCall( 472 | "ns-ntApi", 473 | "nodeIKernelGroupService/operateSysNotify", 474 | [{ 475 | doubt: false, 476 | operateMsg: { 477 | operateType: postData.approve ? 1 : 2, 478 | targetMsg: { 479 | seq: postData.flag, // 通知序列号 480 | type: postData.type || postData.sub_type, 481 | // groupCode: notify.group.groupCode, 482 | postscript: postData.reason, 483 | }, 484 | } 485 | }, null] 486 | ) 487 | }; 488 | } 489 | } 490 | 491 | 492 | /** 493 | * 获取频道系统内BOT的资料 494 | */ 495 | class getGuildProfile extends BaseApi{ 496 | constructor(){ super('get_guild_service_profile'); } 497 | async handle(postData){ 498 | let data = await QQNtAPI.ntCall("ns-GuildInitApi", "ensureLoginInfo", []) 499 | Data.guildInfo.tiny_id = data.tinyId; 500 | 501 | return { 502 | status: 'ok', 503 | retcode: 0, 504 | data: Data.guildInfo 505 | }; 506 | } 507 | } 508 | 509 | 510 | /** 511 | * 获取频道列表 512 | */ 513 | class getGuildList extends BaseApi{ 514 | constructor(){ super('get_guild_list'); } 515 | async handle(postData){ 516 | Data.guilds = await QQNtAPI.getGuildList(); 517 | return { 518 | status: 'ok', 519 | retcode: 0, 520 | data: Object.values(Data.guilds) 521 | } 522 | } 523 | } 524 | 525 | 526 | /** 527 | * 发送信息到子频道 528 | */ 529 | class SendGuildMsg extends BaseApi{ 530 | constructor(){ super('send_guild_channel_msg'); } 531 | async handle(postData){ 532 | let guild = Data.guilds[postData.guild_id]; 533 | if(!guild && guild.channel_list.includes(postData.channel_id)){ 534 | return { 535 | status: 'failed', 536 | retcode: 404, 537 | msg: `找不到频道 (${postData.guild_id}/${postData.channel_id})` 538 | } 539 | } 540 | let peer = { 541 | chatType: 4, 542 | peerUid: postData.channel_id, 543 | guildId: postData.guild_id 544 | } 545 | 546 | let message; 547 | if(postData.message.constructor === String){ 548 | message = [ new Text(postData.message) ] 549 | }else{ 550 | message = [] 551 | for(let item of postData.message){ 552 | message.push(await createOneBot(item, postData.group_id)) 553 | } 554 | } 555 | 556 | let oneBotMsg = new Event.MessageEvent(0, "", postData.user_id, message) 557 | 558 | let qqNtMsg = await QQNtAPI.sendMessage(peer, oneBotMsg.message.map((item) => item.toQQNT())); 559 | 560 | oneBotMsg.time = parseInt(qqNtMsg?.msgTime || 0); 561 | oneBotMsg.message_id = qqNtMsg.msgId; 562 | oneBotMsg.message_type = "guild"; 563 | oneBotMsg.sub_type = "message"; 564 | oneBotMsg.guild_id = qqNtMsg.guildId; 565 | oneBotMsg.channel_id = qqNtMsg.channelId; 566 | 567 | Log.i(`发送频道 (${postData.guildId}/${qqNtMsg.channelId}) 消息:${oneBotMsg.raw_message}`); 568 | 569 | Data.pushHistoryMessage(qqNtMsg, oneBotMsg); 570 | 571 | return { 572 | status: 'ok', 573 | retcode: 0, 574 | data: { message_id: qqNtMsg.msgId } 575 | }; 576 | } 577 | } 578 | 579 | 580 | module.exports = { 581 | api: [ 582 | new BaseApi(''), 583 | 584 | new NtCall(), 585 | new NtCallAsync(), 586 | 587 | new GetFriendList(), 588 | 589 | new SendLike(), 590 | 591 | new GetGroupInfo(), 592 | new GetGroupMemberList(), 593 | new GetGroupMemberInfo(), 594 | new getGroupList(), 595 | new getGroupMsgMask(), 596 | 597 | new KickMember(), 598 | 599 | new setGroupAdmin(), 600 | new setGroupBan(), 601 | new setGroupWholeBan(), 602 | new setGroupName(), 603 | new setGroupCard(), 604 | new setGroupLeave(), 605 | 606 | new SendGuildMsg(), 607 | new getGuildList(), 608 | new getGuildProfile() 609 | ] 610 | } -------------------------------------------------------------------------------- /src/lib/websocket-server.js: -------------------------------------------------------------------------------- 1 | /* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^Duplex$" }] */ 2 | 3 | 'use strict'; 4 | 5 | const EventEmitter = require('events'); 6 | const http = require('http'); 7 | const { Duplex } = require('stream'); 8 | const { createHash } = require('crypto'); 9 | 10 | const extension = require('./extension'); 11 | const PerMessageDeflate = require('./permessage-deflate'); 12 | const subprotocol = require('./subprotocol'); 13 | const WebSocket = require('./websocket'); 14 | const { GUID, kWebSocket } = require('./constants'); 15 | 16 | const keyRegex = /^[+/0-9A-Za-z]{22}==$/; 17 | 18 | const RUNNING = 0; 19 | const CLOSING = 1; 20 | const CLOSED = 2; 21 | 22 | /** 23 | * Class representing a WebSocket server. 24 | * 25 | * @extends EventEmitter 26 | */ 27 | class WebSocketServer extends EventEmitter { 28 | /** 29 | * Create a `WebSocketServer` instance. 30 | * 31 | * @param {Object} options Configuration options 32 | * @param {Boolean} [options.allowSynchronousEvents=false] Specifies whether 33 | * any of the `'message'`, `'ping'`, and `'pong'` events can be emitted 34 | * multiple times in the same tick 35 | * @param {Boolean} [options.autoPong=true] Specifies whether or not to 36 | * automatically send a pong in response to a ping 37 | * @param {Number} [options.backlog=511] The maximum length of the queue of 38 | * pending connections 39 | * @param {Boolean} [options.clientTracking=true] Specifies whether or not to 40 | * track clients 41 | * @param {Function} [options.handleProtocols] A hook to handle protocols 42 | * @param {String} [options.host] The hostname where to bind the server 43 | * @param {Number} [options.maxPayload=104857600] The maximum allowed message 44 | * size 45 | * @param {Boolean} [options.noServer=false] Enable no server mode 46 | * @param {String} [options.path] Accept only connections matching this path 47 | * @param {(Boolean|Object)} [options.perMessageDeflate=false] Enable/disable 48 | * permessage-deflate 49 | * @param {Number} [options.port] The port where to bind the server 50 | * @param {(http.Server|https.Server)} [options.server] A pre-created HTTP/S 51 | * server to use 52 | * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or 53 | * not to skip UTF-8 validation for text and close messages 54 | * @param {Function} [options.verifyClient] A hook to reject connections 55 | * @param {Function} [options.WebSocket=WebSocket] Specifies the `WebSocket` 56 | * class to use. It must be the `WebSocket` class or class that extends it 57 | * @param {Function} [callback] A listener for the `listening` event 58 | */ 59 | constructor(options, callback) { 60 | super(); 61 | 62 | options = { 63 | allowSynchronousEvents: false, 64 | autoPong: true, 65 | maxPayload: 100 * 1024 * 1024, 66 | skipUTF8Validation: false, 67 | perMessageDeflate: false, 68 | handleProtocols: null, 69 | clientTracking: true, 70 | verifyClient: null, 71 | noServer: false, 72 | backlog: null, // use default (511 as implemented in net.js) 73 | server: null, 74 | host: null, 75 | path: null, 76 | port: null, 77 | WebSocket, 78 | ...options 79 | }; 80 | 81 | if ( 82 | (options.port == null && !options.server && !options.noServer) || 83 | (options.port != null && (options.server || options.noServer)) || 84 | (options.server && options.noServer) 85 | ) { 86 | throw new TypeError( 87 | 'One and only one of the "port", "server", or "noServer" options ' + 88 | 'must be specified' 89 | ); 90 | } 91 | 92 | if (options.port != null) { 93 | this._server = http.createServer((req, res) => { 94 | const body = http.STATUS_CODES[426]; 95 | 96 | res.writeHead(426, { 97 | 'Content-Length': body.length, 98 | 'Content-Type': 'text/plain' 99 | }); 100 | res.end(body); 101 | }); 102 | this._server.listen( 103 | options.port, 104 | options.host, 105 | options.backlog, 106 | callback 107 | ); 108 | } else if (options.server) { 109 | this._server = options.server; 110 | } 111 | 112 | if (this._server) { 113 | const emitConnection = this.emit.bind(this, 'connection'); 114 | 115 | this._removeListeners = addListeners(this._server, { 116 | listening: this.emit.bind(this, 'listening'), 117 | error: this.emit.bind(this, 'error'), 118 | upgrade: (req, socket, head) => { 119 | this.handleUpgrade(req, socket, head, emitConnection); 120 | } 121 | }); 122 | } 123 | 124 | if (options.perMessageDeflate === true) options.perMessageDeflate = {}; 125 | if (options.clientTracking) { 126 | this.clients = new Set(); 127 | this._shouldEmitClose = false; 128 | } 129 | 130 | this.options = options; 131 | this._state = RUNNING; 132 | } 133 | 134 | /** 135 | * Returns the bound address, the address family name, and port of the server 136 | * as reported by the operating system if listening on an IP socket. 137 | * If the server is listening on a pipe or UNIX domain socket, the name is 138 | * returned as a string. 139 | * 140 | * @return {(Object|String|null)} The address of the server 141 | * @public 142 | */ 143 | address() { 144 | if (this.options.noServer) { 145 | throw new Error('The server is operating in "noServer" mode'); 146 | } 147 | 148 | if (!this._server) return null; 149 | return this._server.address(); 150 | } 151 | 152 | /** 153 | * Stop the server from accepting new connections and emit the `'close'` event 154 | * when all existing connections are closed. 155 | * 156 | * @param {Function} [cb] A one-time listener for the `'close'` event 157 | * @public 158 | */ 159 | close(cb) { 160 | if (this._state === CLOSED) { 161 | if (cb) { 162 | this.once('close', () => { 163 | cb(new Error('The server is not running')); 164 | }); 165 | } 166 | 167 | process.nextTick(emitClose, this); 168 | return; 169 | } 170 | 171 | if (cb) this.once('close', cb); 172 | 173 | if (this._state === CLOSING) return; 174 | this._state = CLOSING; 175 | 176 | if (this.options.noServer || this.options.server) { 177 | if (this._server) { 178 | this._removeListeners(); 179 | this._removeListeners = this._server = null; 180 | } 181 | 182 | if (this.clients) { 183 | if (!this.clients.size) { 184 | process.nextTick(emitClose, this); 185 | } else { 186 | this._shouldEmitClose = true; 187 | } 188 | } else { 189 | process.nextTick(emitClose, this); 190 | } 191 | } else { 192 | const server = this._server; 193 | 194 | this._removeListeners(); 195 | this._removeListeners = this._server = null; 196 | 197 | // 198 | // The HTTP/S server was created internally. Close it, and rely on its 199 | // `'close'` event. 200 | // 201 | server.close(() => { 202 | emitClose(this); 203 | }); 204 | } 205 | } 206 | 207 | /** 208 | * See if a given request should be handled by this server instance. 209 | * 210 | * @param {http.IncomingMessage} req Request object to inspect 211 | * @return {Boolean} `true` if the request is valid, else `false` 212 | * @public 213 | */ 214 | shouldHandle(req) { 215 | if (this.options.path) { 216 | const index = req.url.indexOf('?'); 217 | const pathname = index !== -1 ? req.url.slice(0, index) : req.url; 218 | 219 | if (pathname !== this.options.path) return false; 220 | } 221 | 222 | return true; 223 | } 224 | 225 | /** 226 | * Handle a HTTP Upgrade request. 227 | * 228 | * @param {http.IncomingMessage} req The request object 229 | * @param {Duplex} socket The network socket between the server and client 230 | * @param {Buffer} head The first packet of the upgraded stream 231 | * @param {Function} cb Callback 232 | * @public 233 | */ 234 | handleUpgrade(req, socket, head, cb) { 235 | socket.on('error', socketOnError); 236 | 237 | const key = req.headers['sec-websocket-key']; 238 | const version = +req.headers['sec-websocket-version']; 239 | 240 | if (req.method !== 'GET') { 241 | const message = 'Invalid HTTP method'; 242 | abortHandshakeOrEmitwsClientError(this, req, socket, 405, message); 243 | return; 244 | } 245 | 246 | if (req.headers.upgrade.toLowerCase() !== 'websocket') { 247 | const message = 'Invalid Upgrade header'; 248 | abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); 249 | return; 250 | } 251 | 252 | if (!key || !keyRegex.test(key)) { 253 | const message = 'Missing or invalid Sec-WebSocket-Key header'; 254 | abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); 255 | return; 256 | } 257 | 258 | if (version !== 8 && version !== 13) { 259 | const message = 'Missing or invalid Sec-WebSocket-Version header'; 260 | abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); 261 | return; 262 | } 263 | 264 | if (!this.shouldHandle(req)) { 265 | abortHandshake(socket, 400); 266 | return; 267 | } 268 | 269 | const secWebSocketProtocol = req.headers['sec-websocket-protocol']; 270 | let protocols = new Set(); 271 | 272 | if (secWebSocketProtocol !== undefined) { 273 | try { 274 | protocols = subprotocol.parse(secWebSocketProtocol); 275 | } catch (err) { 276 | const message = 'Invalid Sec-WebSocket-Protocol header'; 277 | abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); 278 | return; 279 | } 280 | } 281 | 282 | const secWebSocketExtensions = req.headers['sec-websocket-extensions']; 283 | const extensions = {}; 284 | 285 | if ( 286 | this.options.perMessageDeflate && 287 | secWebSocketExtensions !== undefined 288 | ) { 289 | const perMessageDeflate = new PerMessageDeflate( 290 | this.options.perMessageDeflate, 291 | true, 292 | this.options.maxPayload 293 | ); 294 | 295 | try { 296 | const offers = extension.parse(secWebSocketExtensions); 297 | 298 | if (offers[PerMessageDeflate.extensionName]) { 299 | perMessageDeflate.accept(offers[PerMessageDeflate.extensionName]); 300 | extensions[PerMessageDeflate.extensionName] = perMessageDeflate; 301 | } 302 | } catch (err) { 303 | const message = 304 | 'Invalid or unacceptable Sec-WebSocket-Extensions header'; 305 | abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); 306 | return; 307 | } 308 | } 309 | 310 | // 311 | // Optionally call external client verification handler. 312 | // 313 | if (this.options.verifyClient) { 314 | const info = { 315 | origin: 316 | req.headers[`${version === 8 ? 'sec-websocket-origin' : 'origin'}`], 317 | secure: !!(req.socket.authorized || req.socket.encrypted), 318 | req 319 | }; 320 | 321 | if (this.options.verifyClient.length === 2) { 322 | this.options.verifyClient(info, (verified, code, message, headers) => { 323 | if (!verified) { 324 | return abortHandshake(socket, code || 401, message, headers); 325 | } 326 | 327 | this.completeUpgrade( 328 | extensions, 329 | key, 330 | protocols, 331 | req, 332 | socket, 333 | head, 334 | cb 335 | ); 336 | }); 337 | return; 338 | } 339 | 340 | if (!this.options.verifyClient(info)) return abortHandshake(socket, 401); 341 | } 342 | 343 | this.completeUpgrade(extensions, key, protocols, req, socket, head, cb); 344 | } 345 | 346 | /** 347 | * Upgrade the connection to WebSocket. 348 | * 349 | * @param {Object} extensions The accepted extensions 350 | * @param {String} key The value of the `Sec-WebSocket-Key` header 351 | * @param {Set} protocols The subprotocols 352 | * @param {http.IncomingMessage} req The request object 353 | * @param {Duplex} socket The network socket between the server and client 354 | * @param {Buffer} head The first packet of the upgraded stream 355 | * @param {Function} cb Callback 356 | * @throws {Error} If called more than once with the same socket 357 | * @private 358 | */ 359 | completeUpgrade(extensions, key, protocols, req, socket, head, cb) { 360 | // 361 | // Destroy the socket if the client has already sent a FIN packet. 362 | // 363 | if (!socket.readable || !socket.writable) return socket.destroy(); 364 | 365 | if (socket[kWebSocket]) { 366 | throw new Error( 367 | 'server.handleUpgrade() was called more than once with the same ' + 368 | 'socket, possibly due to a misconfiguration' 369 | ); 370 | } 371 | 372 | if (this._state > RUNNING) return abortHandshake(socket, 503); 373 | 374 | const digest = createHash('sha1') 375 | .update(key + GUID) 376 | .digest('base64'); 377 | 378 | const headers = [ 379 | 'HTTP/1.1 101 Switching Protocols', 380 | 'Upgrade: websocket', 381 | 'Connection: Upgrade', 382 | `Sec-WebSocket-Accept: ${digest}` 383 | ]; 384 | 385 | const ws = new this.options.WebSocket(null, undefined, this.options); 386 | 387 | if (protocols.size) { 388 | // 389 | // Optionally call external protocol selection handler. 390 | // 391 | const protocol = this.options.handleProtocols 392 | ? this.options.handleProtocols(protocols, req) 393 | : protocols.values().next().value; 394 | 395 | if (protocol) { 396 | headers.push(`Sec-WebSocket-Protocol: ${protocol}`); 397 | ws._protocol = protocol; 398 | } 399 | } 400 | 401 | if (extensions[PerMessageDeflate.extensionName]) { 402 | const params = extensions[PerMessageDeflate.extensionName].params; 403 | const value = extension.format({ 404 | [PerMessageDeflate.extensionName]: [params] 405 | }); 406 | headers.push(`Sec-WebSocket-Extensions: ${value}`); 407 | ws._extensions = extensions; 408 | } 409 | 410 | // 411 | // Allow external modification/inspection of handshake headers. 412 | // 413 | this.emit('headers', headers, req); 414 | 415 | socket.write(headers.concat('\r\n').join('\r\n')); 416 | socket.removeListener('error', socketOnError); 417 | 418 | ws.setSocket(socket, head, { 419 | allowSynchronousEvents: this.options.allowSynchronousEvents, 420 | maxPayload: this.options.maxPayload, 421 | skipUTF8Validation: this.options.skipUTF8Validation 422 | }); 423 | 424 | if (this.clients) { 425 | this.clients.add(ws); 426 | ws.on('close', () => { 427 | this.clients.delete(ws); 428 | 429 | if (this._shouldEmitClose && !this.clients.size) { 430 | process.nextTick(emitClose, this); 431 | } 432 | }); 433 | } 434 | 435 | cb(ws, req); 436 | } 437 | } 438 | 439 | module.exports = WebSocketServer; 440 | 441 | /** 442 | * Add event listeners on an `EventEmitter` using a map of 443 | * pairs. 444 | * 445 | * @param {EventEmitter} server The event emitter 446 | * @param {Object.} map The listeners to add 447 | * @return {Function} A function that will remove the added listeners when 448 | * called 449 | * @private 450 | */ 451 | function addListeners(server, map) { 452 | for (const event of Object.keys(map)) server.on(event, map[event]); 453 | 454 | return function removeListeners() { 455 | for (const event of Object.keys(map)) { 456 | server.removeListener(event, map[event]); 457 | } 458 | }; 459 | } 460 | 461 | /** 462 | * Emit a `'close'` event on an `EventEmitter`. 463 | * 464 | * @param {EventEmitter} server The event emitter 465 | * @private 466 | */ 467 | function emitClose(server) { 468 | server._state = CLOSED; 469 | server.emit('close'); 470 | } 471 | 472 | /** 473 | * Handle socket errors. 474 | * 475 | * @private 476 | */ 477 | function socketOnError() { 478 | this.destroy(); 479 | } 480 | 481 | /** 482 | * Close the connection when preconditions are not fulfilled. 483 | * 484 | * @param {Duplex} socket The socket of the upgrade request 485 | * @param {Number} code The HTTP response status code 486 | * @param {String} [message] The HTTP response body 487 | * @param {Object} [headers] Additional HTTP response headers 488 | * @private 489 | */ 490 | function abortHandshake(socket, code, message, headers) { 491 | // 492 | // The socket is writable unless the user destroyed or ended it before calling 493 | // `server.handleUpgrade()` or in the `verifyClient` function, which is a user 494 | // error. Handling this does not make much sense as the worst that can happen 495 | // is that some of the data written by the user might be discarded due to the 496 | // call to `socket.end()` below, which triggers an `'error'` event that in 497 | // turn causes the socket to be destroyed. 498 | // 499 | message = message || http.STATUS_CODES[code]; 500 | headers = { 501 | Connection: 'close', 502 | 'Content-Type': 'text/html', 503 | 'Content-Length': Buffer.byteLength(message), 504 | ...headers 505 | }; 506 | 507 | socket.once('finish', socket.destroy); 508 | 509 | socket.end( 510 | `HTTP/1.1 ${code} ${http.STATUS_CODES[code]}\r\n` + 511 | Object.keys(headers) 512 | .map((h) => `${h}: ${headers[h]}`) 513 | .join('\r\n') + 514 | '\r\n\r\n' + 515 | message 516 | ); 517 | } 518 | 519 | /** 520 | * Emit a `'wsClientError'` event on a `WebSocketServer` if there is at least 521 | * one listener for it, otherwise call `abortHandshake()`. 522 | * 523 | * @param {WebSocketServer} server The WebSocket server 524 | * @param {http.IncomingMessage} req The request object 525 | * @param {Duplex} socket The socket of the upgrade request 526 | * @param {Number} code The HTTP response status code 527 | * @param {String} message The HTTP response body 528 | * @private 529 | */ 530 | function abortHandshakeOrEmitwsClientError(server, req, socket, code, message) { 531 | if (server.listenerCount('wsClientError')) { 532 | const err = new Error(message); 533 | Error.captureStackTrace(err, abortHandshakeOrEmitwsClientError); 534 | 535 | server.emit('wsClientError', err, socket, req); 536 | } else { 537 | abortHandshake(socket, code, message); 538 | } 539 | } 540 | --------------------------------------------------------------------------------