+1
-1
@@ -166,3 +166,3 @@ /// <reference types="node" /> | ||
| sex: Gender; | ||
| age: number; /** 配置 */ | ||
| age: number; | ||
| area: string; | ||
@@ -169,0 +169,0 @@ }>; |
+6
-0
@@ -25,4 +25,10 @@ /** 调用API时可能出现的错误 */ | ||
| NoUploadChannel = -130, | ||
| /** 不支持的file类型(没有流) */ | ||
| HighwayFileTypeError = -140, | ||
| /** 文件安全校验未通过不存在 */ | ||
| UnsafeFile = -150, | ||
| /** 离线(私聊)文件不存在 */ | ||
| OfflineFileNotExists = -160, | ||
| /** 群文件不存在(无法转发) */ | ||
| GroupFileNotExists = -170, | ||
| /** 获取视频中的图片失败 */ | ||
@@ -29,0 +35,0 @@ FFmpegVideoThumbError = -210, |
+6
-0
@@ -30,4 +30,10 @@ "use strict"; | ||
| ErrorCode[ErrorCode["NoUploadChannel"] = -130] = "NoUploadChannel"; | ||
| /** 不支持的file类型(没有流) */ | ||
| ErrorCode[ErrorCode["HighwayFileTypeError"] = -140] = "HighwayFileTypeError"; | ||
| /** 文件安全校验未通过不存在 */ | ||
| ErrorCode[ErrorCode["UnsafeFile"] = -150] = "UnsafeFile"; | ||
| /** 离线(私聊)文件不存在 */ | ||
| ErrorCode[ErrorCode["OfflineFileNotExists"] = -160] = "OfflineFileNotExists"; | ||
| /** 群文件不存在(无法转发) */ | ||
| ErrorCode[ErrorCode["GroupFileNotExists"] = -170] = "GroupFileNotExists"; | ||
| /** 获取视频中的图片失败 */ | ||
@@ -34,0 +40,0 @@ ErrorCode[ErrorCode["FFmpegVideoThumbError"] = -210] = "FFmpegVideoThumbError"; |
+5
-2
| /// <reference types="node" /> | ||
| import { pb } from "./core"; | ||
| import { Gender } from "./common"; | ||
| import { Sendable, PrivateMessage, MusicPlatform, Quotable } from "./message"; | ||
| import { Sendable, PrivateMessage, MusicPlatform, Quotable, FileElem } from "./message"; | ||
| import { Contactable } from "./internal"; | ||
@@ -60,2 +60,3 @@ import { MessageRet } from "./events"; | ||
| setGroupInvite(gid: number, seq: number, yes?: boolean, block?: boolean): Promise<boolean>; | ||
| getFileInfo(fid: string): Promise<Omit<FileElem, "type"> & Record<"url", string>>; | ||
| /** 获取离线文件下载地址 */ | ||
@@ -95,6 +96,8 @@ getFileUrl(fid: string): Promise<string>; | ||
| */ | ||
| sendFile(file: string | Buffer, filename?: string, callback?: (percentage: string) => void): Promise<string>; | ||
| sendFile(file: string | Buffer | Uint8Array, filename?: string, callback?: (percentage: string) => void): Promise<string>; | ||
| /** 撤回离线文件 */ | ||
| recallFile(fid: string): Promise<boolean>; | ||
| /** 转发离线文件 */ | ||
| forwardFile(fid: string): Promise<string>; | ||
| } | ||
| export {}; |
+93
-40
@@ -248,4 +248,3 @@ "use strict"; | ||
| } | ||
| /** 获取离线文件下载地址 */ | ||
| async getFileUrl(fid) { | ||
| async getFileInfo(fid) { | ||
| const body = core_1.pb.encode({ | ||
@@ -270,4 +269,15 @@ 1: 1200, | ||
| url = `http://${obj[30]}:${obj[40]}` + url; | ||
| return url; | ||
| return { | ||
| name: String(rsp[40][7]), | ||
| fid: String(rsp[40][6]), | ||
| md5: rsp[40][100].toHex(), | ||
| size: rsp[40][3], | ||
| duration: rsp[40][4], | ||
| url, | ||
| }; | ||
| } | ||
| /** 获取离线文件下载地址 */ | ||
| async getFileUrl(fid) { | ||
| return (await this.getFileInfo(fid)).url; | ||
| } | ||
| } | ||
@@ -379,3 +389,5 @@ exports.User = User; | ||
| let filesize, filemd5, filesha, filestream; | ||
| if (file instanceof Buffer) { | ||
| if (file instanceof Uint8Array) { | ||
| if (!Buffer.isBuffer(file)) | ||
| file = Buffer.from(file); | ||
| filesize = file.length; | ||
@@ -415,41 +427,45 @@ filemd5 = (0, common_1.md5)(file), filesha = (0, common_1.sha)(file); | ||
| const rsp1700 = core_1.pb.decode(payload)[19]; | ||
| if (rsp1700[10] !== 0) | ||
| (0, errors_1.drop)(rsp1700[10], rsp1700[20]); | ||
| const fid = rsp1700[90].toBuffer(); | ||
| const ext = core_1.pb.encode({ | ||
| 1: 100, | ||
| 2: 2, | ||
| 100: { | ||
| if (!rsp1700[110]) { | ||
| const ext = core_1.pb.encode({ | ||
| 1: 100, | ||
| 2: 2, | ||
| 100: { | ||
| 1: 3, | ||
| 100: this.c.uin, | ||
| 200: this.uid, | ||
| 400: 0, | ||
| 700: payload, | ||
| 100: { | ||
| 1: 3, | ||
| 100: this.c.uin, | ||
| 200: this.uid, | ||
| 400: 0, | ||
| 700: payload, | ||
| }, | ||
| 200: { | ||
| 100: filesize, | ||
| 200: filemd5, | ||
| 300: filesha, | ||
| 400: filemd5, | ||
| 600: fid, | ||
| 700: rsp1700[220].toBuffer(), | ||
| }, | ||
| 300: { | ||
| 100: 2, | ||
| 200: String(this.c.apk.subid), | ||
| 300: 2, | ||
| 400: "d92615c5", | ||
| 600: 4, | ||
| }, | ||
| 400: { | ||
| 100: filename, | ||
| }, | ||
| }, | ||
| 200: { | ||
| 100: filesize, | ||
| 200: filemd5, | ||
| 300: filesha, | ||
| 400: filemd5, | ||
| 600: fid, | ||
| 700: rsp1700[220].toBuffer(), | ||
| }, | ||
| 300: { | ||
| 100: 2, | ||
| 200: String(this.c.apk.subid), | ||
| 300: 2, | ||
| 400: "d92615c5", | ||
| 600: 4, | ||
| }, | ||
| 400: { | ||
| 100: filename, | ||
| }, | ||
| }, | ||
| 200: 1 | ||
| }); | ||
| await internal_1.highwayHttpUpload.call(this.c, filestream, { | ||
| md5: filemd5, | ||
| size: filesize, | ||
| cmdid: internal_1.CmdID.OfflineFile, | ||
| ext, callback | ||
| }); | ||
| 200: 1 | ||
| }); | ||
| await internal_1.highwayHttpUpload.call(this.c, filestream, { | ||
| md5: filemd5, | ||
| size: filesize, | ||
| cmdid: internal_1.CmdID.OfflineFile, | ||
| ext, callback | ||
| }); | ||
| } | ||
| const body800 = core_1.pb.encode({ | ||
@@ -499,3 +515,40 @@ 1: 800, | ||
| } | ||
| /** 转发离线文件 */ | ||
| async forwardFile(fid) { | ||
| const body = core_1.pb.encode({ | ||
| 1: 700, | ||
| 2: 0, | ||
| 9: { | ||
| 10: this.c.uin, | ||
| 20: this.uid, | ||
| 30: fid | ||
| }, | ||
| 101: 3, | ||
| 102: 104, | ||
| 200: 1, | ||
| }); | ||
| const payload = await this.c.sendUni("OfflineFilleHandleSvr.pb_ftn_CMD_REQ_APPLY_FORWARD_FILE-700", body); | ||
| const rsp = core_1.pb.decode(payload)[9]; | ||
| const new_fid = rsp[50]; | ||
| const ticket = rsp[60]; | ||
| if (rsp[10] !== 0) | ||
| (0, errors_1.drop)(rsp[10], rsp[20]); | ||
| const info = await this.getFileInfo(fid); | ||
| const proto3 = { | ||
| 2: { | ||
| 1: { | ||
| 1: 0, | ||
| 3: new_fid, | ||
| 4: Buffer.from(info.md5, "hex"), | ||
| 5: info.name, | ||
| 6: info.size, | ||
| 9: 1, | ||
| 57: ticket | ||
| } | ||
| } | ||
| }; | ||
| await this._sendMsg(proto3, `[文件:${info.name}]`, true); | ||
| return String(new_fid); | ||
| } | ||
| } | ||
| exports.Friend = Friend; |
+7
-0
@@ -78,2 +78,9 @@ /// <reference types="node" /> | ||
| upload(file: string | Buffer | Uint8Array, pid?: string, name?: string, callback?: (percentage: string) => void): Promise<GfsFileStat>; | ||
| /** | ||
| * 将文件转发到当前群 | ||
| * @param stat 另一个群中的文件属性() | ||
| * @param pid 转发后的目录(默认根目录) | ||
| * @param name 转发后的文件名(默认不变) | ||
| */ | ||
| forward(stat: GfsFileStat, pid?: string, name?: string): Promise<GfsFileStat>; | ||
| /** 获取文件下载地址 */ | ||
@@ -80,0 +87,0 @@ download(fid: string): Promise<Omit<FileElem, "type"> & { |
+29
-0
@@ -359,2 +359,31 @@ "use strict"; | ||
| } | ||
| /** | ||
| * 将文件转发到当前群 | ||
| * @param stat 另一个群中的文件属性() | ||
| * @param pid 转发后的目录(默认根目录) | ||
| * @param name 转发后的文件名(默认不变) | ||
| */ | ||
| async forward(stat, pid = "/", name) { | ||
| const body = core_1.pb.encode({ | ||
| 1: { | ||
| 1: this.gid, | ||
| 2: 3, | ||
| 3: 102, | ||
| 4: 5, | ||
| 5: String(pid), | ||
| 6: String(name || stat.name), | ||
| 7: "/storage/emulated/0/Pictures/files/s/" + (name || stat.name), | ||
| 8: Number(stat.size), | ||
| 9: Buffer.from(stat.sha1, "hex"), | ||
| 11: Buffer.from(stat.md5, "hex"), | ||
| 15: 1, | ||
| } | ||
| }); | ||
| const payload = await this.c.sendOidb("OidbSvc.0x6d6_0", body); | ||
| const rsp = core_1.pb.decode(payload)[4][1]; | ||
| checkRsp(rsp); | ||
| if (!rsp[10]) | ||
| (0, errors_1.drop)(errors_1.ErrorCode.GroupFileNotExists, "文件不存在,无法被转发"); | ||
| return await this._feed(String(rsp[7]), rsp[6]); | ||
| } | ||
| /** 获取文件下载地址 */ | ||
@@ -361,0 +390,0 @@ async download(fid) { |
@@ -29,2 +29,3 @@ "use strict"; | ||
| const crypto_1 = require("crypto"); | ||
| const http_1 = __importDefault(require("http")); | ||
| const axios_1 = __importDefault(require("axios")); | ||
@@ -106,2 +107,4 @@ const core_1 = require("../core"); | ||
| throw new core_1.ApiRejection(errors_1.ErrorCode.NoUploadChannel, "没有上传通道,如果你刚刚登录,请等待几秒"); | ||
| if (!readable) | ||
| throw new core_1.ApiRejection(errors_1.ErrorCode.HighwayFileTypeError, "不支持的file类型"); | ||
| this.logger.debug(`highway ip:${ip} port:${port}`); | ||
@@ -169,3 +172,4 @@ return new Promise((resolve, reject) => { | ||
| exports.highwayUpload = highwayUpload; | ||
| async function highwayHttpUpload(readable, obj) { | ||
| const agent = new http_1.default.Agent({ maxSockets: 10 }); | ||
| function highwayHttpUpload(readable, obj) { | ||
| const ip = this.sig.bigdata.ip; | ||
@@ -180,3 +184,4 @@ const port = this.sig.bigdata.port; | ||
| obj.ticket = this.sig.bigdata.sig_session; | ||
| const tasks = []; | ||
| const tasks = new Set(); | ||
| const cancels = new Set(); | ||
| let finished = 0; | ||
@@ -218,4 +223,8 @@ readable.on("data", data => { | ||
| const task = new Promise((resolve, reject) => { | ||
| const c = axios_1.default.CancelToken.source(); | ||
| cancels.add(c); | ||
| axios_1.default.post(url, buf, { | ||
| responseType: "arraybuffer", | ||
| httpAgent: agent, | ||
| cancelToken: c.token, | ||
| headers: { | ||
@@ -241,10 +250,18 @@ "Content-Length": String(buf.length) | ||
| ++finished; | ||
| percentage = (finished / tasks.length * 100).toFixed(2); | ||
| percentage = (finished / tasks.size * 100).toFixed(2); | ||
| this.logger.debug(`highway(http) chunk uploaded (${percentage}%)`); | ||
| if (typeof obj.callback === "function" && percentage) | ||
| obj.callback(percentage); | ||
| if (rsp[7]?.toBuffer().length > 0 && finished < tasks.size) { | ||
| cancels.forEach(c => c.cancel()); | ||
| this.logger.debug(`highway(http) chunk uploaded (100.00%)`); | ||
| if (typeof obj.callback === "function") | ||
| obj.callback("100.00"); | ||
| } | ||
| if (!rsp[7]?.toBuffer().length && finished >= tasks.size) | ||
| reject(new core_1.ApiRejection(errors_1.ErrorCode.UnsafeFile, "文件校验未通过,上传失败")); | ||
| resolve(undefined); | ||
| }); | ||
| }).catch(reject); | ||
| }); | ||
| tasks.push(task); | ||
| tasks.add(task); | ||
| } | ||
@@ -255,5 +272,11 @@ offset += data.length; | ||
| readable.on("err", reject) | ||
| .on("end", () => resolve(Promise.all(tasks))); | ||
| .on("end", () => { | ||
| Promise.all(tasks).then(resolve).catch(err => { | ||
| if (err instanceof axios_1.default.Cancel === false) | ||
| reject(err); | ||
| resolve(undefined); | ||
| }); | ||
| }); | ||
| }); | ||
| } | ||
| exports.highwayHttpUpload = highwayHttpUpload; |
+3
-3
| { | ||
| "name": "oicq", | ||
| "version": "2.3.0", | ||
| "upday": "2022/6/19", | ||
| "version": "2.3.1", | ||
| "upday": "2022/6/21", | ||
| "description": "QQ protocol!", | ||
@@ -29,3 +29,3 @@ "main": "lib/index.js", | ||
| "dependencies": { | ||
| "axios": "^0.24.0", | ||
| "axios": "^0.27.2", | ||
| "log4js": "^6.3.0", | ||
@@ -32,0 +32,0 @@ "long": "^4.0.0", |
+2
-0
@@ -239,2 +239,3 @@ # oicq | ||
| | sendFile() | 发送文件 | | ||
| | forwardFile()| 转发文件 | | ||
| | recallFile() | 撤回文件 | | ||
@@ -312,2 +313,3 @@ | ||
| | upload() | 上传文件 | | ||
| | forward() | 转发文件 | | ||
| | download() | 获取下载链接 | | ||
@@ -314,0 +316,0 @@ |
Network access
Supply chain riskThis module accesses the network.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
AI-detected potential malware
Supply chain riskAI has identified this package as malware. This is a strong signal that the package may be malicious.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
558012
0.95%14000
0.92%398
0.51%0
-100%10
-9.09%10
11.11%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
- Removed
Updated