diff --git a/README.md b/README.md index 081bd51e..a00b37b8 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@
- NapCatQQ + + ![Logo](https://socialify.git.ci/NapNeko/NapCatQQ/image?font=Jost&logo=https%3A%2F%2Fnapneko.github.io%2Fassets%2Flogo.png&name=1&owner=1&pattern=Diagonal%20Stripes&stargazers=1&theme=Auto) +
--- diff --git a/manifest.json b/manifest.json index 5d62896c..c9eac215 100644 --- a/manifest.json +++ b/manifest.json @@ -4,7 +4,7 @@ "name": "NapCatQQ", "slug": "NapCat.Framework", "description": "高性能的 OneBot 11 协议实现", - "version": "3.1.3", + "version": "3.1.5", "icon": "./logo.png", "authors": [ { diff --git a/package.json b/package.json index 7511d43b..583fcf88 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "napcat", "private": true, "type": "module", - "version": "3.1.3", + "version": "3.1.5", "scripts": { "build:framework": "vite build --mode framework", "build:shell": "vite build --mode shell", @@ -21,7 +21,6 @@ "@types/express": "^5.0.0", "@types/fluent-ffmpeg": "^2.1.24", "@types/fs-extra": "^11.0.4", - "@types/jest": "^29.5.12", "@types/node": "^22.0.1", "@types/qrcode-terminal": "^0.12.2", "@types/ws": "^8.5.12", @@ -37,9 +36,8 @@ "eslint-plugin-import": "^2.29.1", "fast-xml-parser": "^4.3.6", "file-type": "^19.0.0", - "fluent-ffmpeg": "^2.1.2", "image-size": "^1.1.1", - "json-schema-to-ts": "^3.1.0", + "json-schema-to-ts": "^3.1.1", "typescript": "^5.3.3", "vite": "5.4.6", "vite-plugin-cp": "^4.0.8", diff --git a/src/common/forward-msg-builder.ts b/src/common/forward-msg-builder.ts index bcd521b0..4c18e72e 100644 --- a/src/common/forward-msg-builder.ts +++ b/src/common/forward-msg-builder.ts @@ -50,9 +50,29 @@ interface ForwardAdaptMsgElement { } export class ForwardMsgBuilder { - private static build(resId: string, msg: ForwardAdaptMsg[]): ForwardMsgJson { + private static build(resId: string, msg: ForwardAdaptMsg[], source?: string, news?: ForwardMsgJsonMetaDetail["news"], summary?: string, prompt?: string): ForwardMsgJson { const id = crypto.randomUUID(); const isGroupMsg = msg.some(m => m.isGroupMsg); + if (!source) { + source = isGroupMsg ? "群聊的聊天记录" : + msg.length + ? Array.from(new Set(msg.map(m => m.senderName))) + .join('和') + '的聊天记录' + : '聊天记录'; + } + if (!news) { + news = msg.length === 0 ? [{ + text: "Nya~ This message is send from NapCat.Packet!", + }] : msg.map(m => ({ + text: `${m.senderName}: ${m.msg?.map(msg => msg.preview).join('')}`, + })); + } + if (!summary) { + summary = `查看${msg.length}条转发消息`; + } + if (!prompt) { + prompt = "[聊天记录]"; + } return { app: "com.tencent.multimsg", config: { @@ -62,29 +82,21 @@ export class ForwardMsgBuilder { type: "normal", width: 300 }, - desc: "[聊天记录]", + desc: prompt, extra: { filename: id, tsum: msg.length, }, meta: { detail: { - news: msg.length === 0 ? [{ - text: "Nya~ This message is send from NapCat.Packet!", - }] : msg.map(m => ({ - text: `${m.senderName}: ${m.msg?.map(msg => msg.preview).join('')}`, - })), + news, resid: resId, - source: isGroupMsg ? "群聊的聊天记录" : - msg.length - ? Array.from(new Set(msg.map(m => m.senderName))) - .join('和') + '的聊天记录' - : '聊天记录', - summary: `查看${msg.length}条转发消息`, + source, + summary, uniseq: id, } }, - prompt: "[聊天记录]", + prompt, ver: "0.0.0.5", view: "contact", }; @@ -94,13 +106,13 @@ export class ForwardMsgBuilder { return this.build(resId, []); } - static fromPacketMsg(resId: string, packetMsg: PacketMsg[]): ForwardMsgJson { + static fromPacketMsg(resId: string, packetMsg: PacketMsg[], source?: string, news?: ForwardMsgJsonMetaDetail["news"], summary?: string, prompt?: string): ForwardMsgJson { return this.build(resId, packetMsg.map(msg => ({ senderName: msg.senderName, isGroupMsg: msg.groupId !== undefined, msg: msg.msg.map(m => ({ - preview: m.toPreview(), + preview: m.valid? m.toPreview() : "[该消息类型暂不支持查看]", })) - }))); + })), source, news, summary, prompt); } } diff --git a/src/common/version.ts b/src/common/version.ts index 4ce85e80..942b9be5 100644 --- a/src/common/version.ts +++ b/src/common/version.ts @@ -1 +1 @@ -export const napCatVersion = '3.1.3'; +export const napCatVersion = '3.1.5'; diff --git a/src/core/apis/packet.ts b/src/core/apis/packet.ts index fb930e95..5e18d1cb 100644 --- a/src/core/apis/packet.ts +++ b/src/core/apis/packet.ts @@ -3,7 +3,7 @@ import { ChatType, InstanceContext, NapCatCore } from '..'; import offset from '@/core/external/offset.json'; import { PacketClient, RecvPacketData } from '@/core/packet/client'; import { PacketSession } from "@/core/packet/session"; -import { PacketHexStr } from "@/core/packet/packer"; +import {OidbPacket, PacketHexStr} from "@/core/packet/packer"; import { NapProtoMsg } from '@/core/packet/proto/NapProto'; import { OidbSvcTrpcTcp0X9067_202_Rsp_Body } from '@/core/packet/proto/oidb/Oidb.0x9067_202'; import { OidbSvcTrpcTcpBase, OidbSvcTrpcTcpBaseRsp } from '@/core/packet/proto/oidb/OidbBase'; @@ -12,7 +12,12 @@ import { LogWrapper } from "@/common/log"; import { SendLongMsgResp } from "@/core/packet/proto/message/action"; import { PacketMsg } from "@/core/packet/msg/message"; import { OidbSvcTrpcTcp0x6D6Response } from "@/core/packet/proto/oidb/Oidb.0x6D6"; -import { PacketMsgPicElement } from "@/core/packet/msg/element"; +import { + PacketMsgFileElement, + PacketMsgPicElement, + PacketMsgPttElement, + PacketMsgVideoElement +} from "@/core/packet/msg/element"; interface OffsetType { @@ -73,25 +78,32 @@ export class NTQQPacketApi { return this.packetSession!.client.sendPacket(cmd, data, rsp); } + async sendOidbPacket(pkt: OidbPacket, rsp = false): Promise { + return this.sendPacket(pkt.cmd, pkt.data, rsp); + } + async sendPokePacket(peer: number, group?: number) { const data = this.packetSession?.packer.packPokePacket(peer, group); - await this.sendPacket('OidbSvcTrpcTcp.0xed3_1', data!, false); + await this.sendOidbPacket(data!, false); } async sendRkeyPacket() { const packet = this.packetSession?.packer.packRkeyPacket(); - const ret = await this.sendPacket('OidbSvcTrpcTcp.0x9067_202', packet!, true); + const ret = await this.sendOidbPacket(packet!, true); if (!ret?.hex_data) return []; const body = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(Buffer.from(ret.hex_data, 'hex')).body; const retData = new NapProtoMsg(OidbSvcTrpcTcp0X9067_202_Rsp_Body).decode(body); return retData.data.rkeyList; } - + async sendGroupSignPacket(groupCode: string) { + const packet = this.packetSession?.packer.packGroupSignReq(this.core.selfInfo.uin, groupCode); + await this.sendOidbPacket(packet!, true); + } async sendStatusPacket(uin: number): Promise<{ status: number; ext_status: number; } | undefined> { let status = 0; try { const packet = this.packetSession?.packer.packStatusPacket(uin); - const ret = await this.sendPacket('OidbSvcTrpcTcp.0xfe1_2', packet!, true); + const ret = await this.sendOidbPacket( packet!, true); const data = Buffer.from(ret.hex_data, 'hex'); const ext = new NapProtoMsg(OidbSvcTrpcTcp0XFE1_2RSP).decode(new NapProtoMsg(OidbSvcTrpcTcpBase).decode(data).body).data.status.value; // ext & 0xff00 + ext >> 16 & 0xff @@ -108,22 +120,47 @@ export class NTQQPacketApi { async sendSetSpecialTittlePacket(groupCode: string, uid: string, tittle: string) { const data = this.packetSession?.packer.packSetSpecialTittlePacket(groupCode, uid, tittle); - await this.sendPacket('OidbSvcTrpcTcp.0x8fc_2', data!, true); + await this.sendOidbPacket(data!, true); } - private async uploadResources(msg: PacketMsg[], groupUin: number = 0) { + // TODO: can simplify this + async uploadResources(msg: PacketMsg[], groupUin: number = 0) { const reqList = []; for (const m of msg) { for (const e of m.msg) { if (e instanceof PacketMsgPicElement) { reqList.push(this.packetSession?.highwaySession.uploadImage({ chatType: groupUin ? ChatType.KCHATTYPEGROUP : ChatType.KCHATTYPEC2C, - peerUid: String(groupUin) ? String(groupUin) : this.core.selfInfo.uid + peerUid: groupUin ? String(groupUin) : this.core.selfInfo.uid + }, e)); + } + if (e instanceof PacketMsgVideoElement) { + reqList.push(this.packetSession?.highwaySession.uploadVideo({ + chatType: groupUin ? ChatType.KCHATTYPEGROUP : ChatType.KCHATTYPEC2C, + peerUid: groupUin ? String(groupUin) : this.core.selfInfo.uid + }, e)); + } + if (e instanceof PacketMsgPttElement) { + reqList.push(this.packetSession?.highwaySession.uploadPtt({ + chatType: groupUin ? ChatType.KCHATTYPEGROUP : ChatType.KCHATTYPEC2C, + peerUid: groupUin ? String(groupUin) : this.core.selfInfo.uid + }, e)); + } + if (e instanceof PacketMsgFileElement){ + reqList.push(this.packetSession?.highwaySession.uploadFile({ + chatType: groupUin ? ChatType.KCHATTYPEGROUP : ChatType.KCHATTYPEC2C, + peerUid: groupUin ? String(groupUin) : this.core.selfInfo.uid }, e)); } } } - return Promise.all(reqList); + const res = await Promise.allSettled(reqList); + this.logger.log(`上传资源${res.length}个, 失败${res.filter(r => r.status === 'rejected').length}个`); + res.forEach((result, index) => { + if (result.status === 'rejected') { + this.logger.logError(`第${index + 1}个失败:${result.reason}`); + } + }); } async sendUploadForwardMsg(msg: PacketMsg[], groupUin: number = 0) { @@ -137,7 +174,7 @@ export class NTQQPacketApi { async sendGroupFileDownloadReq(groupUin: number, fileUUID: string) { const data = this.packetSession?.packer.packGroupFileDownloadReq(groupUin, fileUUID); - const ret = await this.sendPacket('OidbSvcTrpcTcp.0x6d6_2', data!, true); + const ret = await this.sendOidbPacket(data!, true); const body = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(Buffer.from(ret.hex_data, 'hex')).body; const resp = new NapProtoMsg(OidbSvcTrpcTcp0x6D6Response).decode(body); if (resp.download.retCode !== 0) { diff --git a/src/core/external/appid.json b/src/core/external/appid.json index 2d2f05f9..5dd23607 100644 --- a/src/core/external/appid.json +++ b/src/core/external/appid.json @@ -50,5 +50,13 @@ "9.9.16-28788": { "appid": 537249739, "qua": "V1_WIN_NQ_9.9.16_28788_GW_B" + }, + "9.9.16-28971":{ + "appid": 537249775, + "qua": "V1_WIN_NQ_9.9.16_28971_GW_B" + }, + "3.2.13-28971": { + "appid": 537249848, + "qua": "V1_LNX_NQ_3.2.13_28971_GW_B" } } \ No newline at end of file diff --git a/src/core/external/offset.json b/src/core/external/offset.json index 91c5983d..f2f01f21 100644 --- a/src/core/external/offset.json +++ b/src/core/external/offset.json @@ -19,8 +19,20 @@ "send": "A0CEC20", "recv": "A0D2520" }, - "3.2.13-28788-arm64":{ + "3.2.13-28788-arm64": { "send": "6E91018", "recv": "6E94850" + }, + "9.9.16-28971-x64": { + "send": "38079F0", + "recv": "380BE24" + }, + "3.2.13-28971-x64": { + "send": "A0CEF60", + "recv": "A0D2860" + }, + "3.2.12-28971-arm64": { + "send": "6E91318", + "recv": "6E94B50" } } \ No newline at end of file diff --git a/src/core/packet/client.ts b/src/core/packet/client.ts index 441c3465..f4e0a27c 100644 --- a/src/core/packet/client.ts +++ b/src/core/packet/client.ts @@ -3,8 +3,7 @@ import { LRUCache } from "@/common/lru-cache"; import WebSocket, { Data } from "ws"; import crypto, { createHash } from "crypto"; import { NapCatCore } from "@/core"; -import { PacketHexStr } from "@/core/packet/packer"; -import { sleep } from "@/common/helper"; +import { OidbPacket, PacketHexStr } from "@/core/packet/packer"; export interface RecvPacket { type: string, // 仅recv @@ -177,4 +176,8 @@ export class PacketClient { }).then((res) => resolve(res)).catch((e: Error) => reject(e)); }); } + + async sendOidbPacket(pkt: OidbPacket, rsp = false): Promise { + return this.sendPacket(pkt.cmd, pkt.data, rsp); + } } diff --git a/src/core/packet/highway/client.ts b/src/core/packet/highway/client.ts index dcf4ca5c..4c788568 100644 --- a/src/core/packet/highway/client.ts +++ b/src/core/packet/highway/client.ts @@ -36,7 +36,7 @@ export class PacketHighwayClient { this.port = port; } - private buildDataUpTrans(cmd: number, data: ReadStream, fileSize: number, md5: Uint8Array, extendInfo: Uint8Array, timeout: number = 3600): PacketHighwayTrans { + private buildDataUpTrans(cmd: number, data: ReadStream, fileSize: number, md5: Uint8Array, extendInfo: Uint8Array, timeout: number = 1200): PacketHighwayTrans { return { uin: this.sig.uin, cmd: cmd, diff --git a/src/core/packet/highway/session.ts b/src/core/packet/highway/session.ts index 54384840..0f3ae108 100644 --- a/src/core/packet/highway/session.ts +++ b/src/core/packet/highway/session.ts @@ -8,9 +8,17 @@ import { HttpConn0x6ff_501Response } from "@/core/packet/proto/action/action"; import { PacketHighwayClient } from "@/core/packet/highway/client"; import { NTV2RichMediaResp } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaResp"; import { OidbSvcTrpcTcpBaseRsp } from "@/core/packet/proto/oidb/OidbBase"; -import { PacketMsgPicElement } from "@/core/packet/msg/element"; -import { NTV2RichMediaHighwayExt } from "@/core/packet/proto/highway/highway"; +import { + PacketMsgFileElement, + PacketMsgPicElement, + PacketMsgPttElement, + PacketMsgVideoElement +} from "@/core/packet/msg/element"; +import { FileUploadExt, NTV2RichMediaHighwayExt } from "@/core/packet/proto/highway/highway"; import { int32ip2str, oidbIpv4s2HighwayIpv4s } from "@/core/packet/highway/utils"; +import { calculateSha1, calculateSha1StreamBytes, computeMd5AndLengthWithLimit } from "@/core/packet/utils/crypto/hash"; +import { OidbSvcTrpcTcp0x6D6Response } from "@/core/packet/proto/oidb/Oidb.0x6D6"; +import { OidbSvcTrpcTcp0XE37_800Response, OidbSvcTrpcTcp0XE37Response } from "@/core/packet/proto/oidb/Oidb.0XE37_800"; export const BlockSize = 1024 * 1024; @@ -21,6 +29,7 @@ interface HighwayServerAddr { export interface PacketHighwaySig { uin: string; + uid: string; sigSession: Uint8Array | null sessionKey: Uint8Array | null serverAddr: HighwayServerAddr[] @@ -39,6 +48,7 @@ export class PacketHighwaySession { this.logger = logger; this.sig = { uin: this.packetClient.napCatCore.selfInfo.uin, + uid: this.packetClient.napCatCore.selfInfo.uid, sigSession: null, sessionKey: null, serverAddr: [], @@ -49,7 +59,6 @@ export class PacketHighwaySession { private async checkAvailable() { if (!this.packetClient.available) { - this.logger.logError('[Highway] packetServer not available!'); throw new Error('packetServer不可用,请参照文档 https://napneko.github.io/config/advanced 检查packetServer状态或进行配置'); } if (this.sig.sigSession === null || this.sig.sessionKey === null) { @@ -86,7 +95,7 @@ export class PacketHighwaySession { async uploadImage(peer: Peer, img: PacketMsgPicElement): Promise { await this.checkAvailable(); if (peer.chatType === ChatType.KCHATTYPEGROUP) { - await this.uploadGroupImageReq(Number(peer.peerUid), img); + await this.uploadGroupImageReq(+peer.peerUid, img); } else if (peer.chatType === ChatType.KCHATTYPEC2C) { await this.uploadC2CImageReq(peer.peerUid, img); } else { @@ -94,16 +103,53 @@ export class PacketHighwaySession { } } + async uploadVideo(peer: Peer, video: PacketMsgVideoElement): Promise { + await this.checkAvailable(); + if (+(video.fileSize ?? 0) > 1024 * 1024 * 100) { + throw new Error(`[Highway] 视频文件过大: ${(+(video.fileSize ?? 0) / (1024 * 1024)).toFixed(2)} MB > 100 MB,请使用文件上传!`); + } + if (peer.chatType === ChatType.KCHATTYPEGROUP) { + await this.uploadGroupVideoReq(+peer.peerUid, video); + } else if (peer.chatType === ChatType.KCHATTYPEC2C) { + await this.uploadC2CVideoReq(peer.peerUid, video); + } else { + throw new Error(`[Highway] unsupported chatType: ${peer.chatType}`); + } + } + + async uploadPtt(peer: Peer, ptt: PacketMsgPttElement): Promise { + await this.checkAvailable(); + if (peer.chatType === ChatType.KCHATTYPEGROUP) { + await this.uploadGroupPttReq(+peer.peerUid, ptt); + } else if (peer.chatType === ChatType.KCHATTYPEC2C) { + await this.uploadC2CPttReq(peer.peerUid, ptt); + } else { + throw new Error(`[Highway] unsupported chatType: ${peer.chatType}`); + } + } + + async uploadFile(peer: Peer, file: PacketMsgFileElement): Promise { + await this.checkAvailable(); + if (peer.chatType === ChatType.KCHATTYPEGROUP) { + await this.uploadGroupFileReq(+peer.peerUid, file); + } else if (peer.chatType === ChatType.KCHATTYPEC2C) { + await this.uploadC2CFileReq(peer.peerUid, file); + } else { + throw new Error(`[Highway] unsupported chatType: ${peer.chatType}`); + } + } + private async uploadGroupImageReq(groupUin: number, img: PacketMsgPicElement): Promise { + img.sha1 = Buffer.from(await calculateSha1(img.path)).toString('hex'); const preReq = await this.packer.packUploadGroupImgReq(groupUin, img); - const preRespRaw = await this.packetClient.sendPacket('OidbSvcTrpcTcp.0x11c4_100', preReq, true); + const preRespRaw = await this.packetClient.sendOidbPacket(preReq, true); const preResp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode( Buffer.from(preRespRaw.hex_data, 'hex') ); const preRespData = new NapProtoMsg(NTV2RichMediaResp).decode(preResp.body); const ukey = preRespData.upload.uKey; if (ukey && ukey != "") { - this.logger.logDebug(`[Highway] get upload ukey: ${ukey}, need upload!`); + this.logger.logDebug(`[Highway] uploadGroupImageReq get upload ukey: ${ukey}, need upload!`); const index = preRespData.upload.msgInfo.msgInfoBody[0].index; const sha1 = Buffer.from(index.info.fileSha1, 'hex'); const md5 = Buffer.from(index.info.fileHash, 'hex'); @@ -121,28 +167,29 @@ export class PacketHighwaySession { }); await this.packetHighwayClient.upload( 1004, - fs.createReadStream(img.path, { highWaterMark: BlockSize }), + fs.createReadStream(img.path, {highWaterMark: BlockSize}), img.size, md5, extend ); } else { - this.logger.logDebug(`[Highway] get upload invalid ukey ${ukey}, don't need upload!`); + this.logger.logDebug(`[Highway] uploadGroupImageReq get upload invalid ukey ${ukey}, don't need upload!`); } img.msgInfo = preRespData.upload.msgInfo; // img.groupPicExt = new NapProtoMsg(CustomFace).decode(preRespData.tcpUpload.compatQMsg) } private async uploadC2CImageReq(peerUid: string, img: PacketMsgPicElement): Promise { + img.sha1 = Buffer.from(await calculateSha1(img.path)).toString('hex'); const preReq = await this.packer.packUploadC2CImgReq(peerUid, img); - const preRespRaw = await this.packetClient.sendPacket('OidbSvcTrpcTcp.0x11c5_100', preReq, true); + const preRespRaw = await this.packetClient.sendOidbPacket(preReq, true); const preResp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode( Buffer.from(preRespRaw.hex_data, 'hex') ); const preRespData = new NapProtoMsg(NTV2RichMediaResp).decode(preResp.body); const ukey = preRespData.upload.uKey; if (ukey && ukey != "") { - this.logger.logDebug(`[Highway] get upload ukey: ${ukey}, need upload!`); + this.logger.logDebug(`[Highway] uploadC2CImageReq get upload ukey: ${ukey}, need upload!`); const index = preRespData.upload.msgInfo.msgInfoBody[0].index; const sha1 = Buffer.from(index.info.fileSha1, 'hex'); const md5 = Buffer.from(index.info.fileHash, 'hex'); @@ -160,12 +207,364 @@ export class PacketHighwaySession { }); await this.packetHighwayClient.upload( 1003, - fs.createReadStream(img.path, { highWaterMark: BlockSize }), + fs.createReadStream(img.path, {highWaterMark: BlockSize}), img.size, md5, extend ); + } else { + this.logger.logDebug(`[Highway] uploadC2CImageReq get upload invalid ukey ${ukey}, don't need upload!`); } img.msgInfo = preRespData.upload.msgInfo; } + + private async uploadGroupVideoReq(groupUin: number, video: PacketMsgVideoElement): Promise { + if (!video.filePath || !video.thumbPath) throw new Error("video.filePath or video.thumbPath is empty"); + video.fileSha1 = Buffer.from(await calculateSha1(video.filePath)).toString('hex'); + video.thumbSha1 = Buffer.from(await calculateSha1(video.thumbPath)).toString('hex'); + const preReq = await this.packer.packUploadGroupVideoReq(groupUin, video); + const preRespRaw = await this.packetClient.sendOidbPacket(preReq, true); + const preResp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode( + Buffer.from(preRespRaw.hex_data, 'hex') + ); + const preRespData = new NapProtoMsg(NTV2RichMediaResp).decode(preResp.body); + const ukey = preRespData.upload.uKey; + if (ukey && ukey != "") { + this.logger.logDebug(`[Highway] uploadGroupVideoReq get upload video ukey: ${ukey}, need upload!`); + const index = preRespData.upload.msgInfo.msgInfoBody[0].index; + const md5 = Buffer.from(index.info.fileHash, 'hex'); + const extend = new NapProtoMsg(NTV2RichMediaHighwayExt).encode({ + fileUuid: index.fileUuid, + uKey: ukey, + network: { + ipv4S: oidbIpv4s2HighwayIpv4s(preRespData.upload.ipv4S) + }, + msgInfoBody: preRespData.upload.msgInfo.msgInfoBody, + blockSize: BlockSize, + hash: { + fileSha1: await calculateSha1StreamBytes(video.filePath!) + } + }) + await this.packetHighwayClient.upload( + 1005, + fs.createReadStream(video.filePath!, {highWaterMark: BlockSize}), + +video.fileSize!, + md5, + extend + ); + } else { + this.logger.logDebug(`[Highway] uploadGroupVideoReq get upload invalid ukey ${ukey}, don't need upload!`); + } + const subFile = preRespData.upload.subFileInfos[0]; + if (subFile.uKey && subFile.uKey != "") { + this.logger.logDebug(`[Highway] uploadGroupVideoReq get upload video thumb ukey: ${subFile.uKey}, need upload!`); + const index = preRespData.upload.msgInfo.msgInfoBody[1].index; + const md5 = Buffer.from(index.info.fileHash, 'hex'); + const sha1 = Buffer.from(index.info.fileSha1, 'hex'); + const extend = new NapProtoMsg(NTV2RichMediaHighwayExt).encode({ + fileUuid: index.fileUuid, + uKey: subFile.uKey, + network: { + ipv4S: oidbIpv4s2HighwayIpv4s(subFile.ipv4S) + }, + msgInfoBody: preRespData.upload.msgInfo.msgInfoBody, + blockSize: BlockSize, + hash: { + fileSha1: [sha1] + } + }); + await this.packetHighwayClient.upload( + 1006, + fs.createReadStream(video.thumbPath!, {highWaterMark: BlockSize}), + +video.thumbSize!, + md5, + extend + ); + } else { + this.logger.logDebug(`[Highway] uploadGroupVideoReq get upload invalid thumb ukey ${subFile.uKey}, don't need upload!`); + } + video.msgInfo = preRespData.upload.msgInfo; + } + + private async uploadC2CVideoReq(peerUid: string, video: PacketMsgVideoElement): Promise { + if (!video.filePath || !video.thumbPath) throw new Error("video.filePath or video.thumbPath is empty"); + video.fileSha1 = Buffer.from(await calculateSha1(video.filePath)).toString('hex'); + video.thumbSha1 = Buffer.from(await calculateSha1(video.thumbPath)).toString('hex'); + const preReq = await this.packer.packUploadC2CVideoReq(peerUid, video); + const preRespRaw = await this.packetClient.sendOidbPacket(preReq, true); + const preResp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode( + Buffer.from(preRespRaw.hex_data, 'hex') + ); + const preRespData = new NapProtoMsg(NTV2RichMediaResp).decode(preResp.body); + const ukey = preRespData.upload.uKey; + if (ukey && ukey != "") { + this.logger.logDebug(`[Highway] uploadC2CVideoReq get upload video ukey: ${ukey}, need upload!`); + const index = preRespData.upload.msgInfo.msgInfoBody[0].index; + const md5 = Buffer.from(index.info.fileHash, 'hex'); + const extend = new NapProtoMsg(NTV2RichMediaHighwayExt).encode({ + fileUuid: index.fileUuid, + uKey: ukey, + network: { + ipv4S: oidbIpv4s2HighwayIpv4s(preRespData.upload.ipv4S) + }, + msgInfoBody: preRespData.upload.msgInfo.msgInfoBody, + blockSize: BlockSize, + hash: { + fileSha1: await calculateSha1StreamBytes(video.filePath!) + } + }) + await this.packetHighwayClient.upload( + 1001, + fs.createReadStream(video.filePath!, {highWaterMark: BlockSize}), + +video.fileSize!, + md5, + extend + ); + } else { + this.logger.logDebug(`[Highway] uploadC2CVideoReq get upload invalid ukey ${ukey}, don't need upload!`); + } + const subFile = preRespData.upload.subFileInfos[0]; + if (subFile.uKey && subFile.uKey != "") { + this.logger.logDebug(`[Highway] uploadC2CVideoReq get upload video thumb ukey: ${subFile.uKey}, need upload!`); + const index = preRespData.upload.msgInfo.msgInfoBody[1].index; + const md5 = Buffer.from(index.info.fileHash, 'hex'); + const sha1 = Buffer.from(index.info.fileSha1, 'hex'); + const extend = new NapProtoMsg(NTV2RichMediaHighwayExt).encode({ + fileUuid: index.fileUuid, + uKey: subFile.uKey, + network: { + ipv4S: oidbIpv4s2HighwayIpv4s(subFile.ipv4S) + }, + msgInfoBody: preRespData.upload.msgInfo.msgInfoBody, + blockSize: BlockSize, + hash: { + fileSha1: [sha1] + } + }); + await this.packetHighwayClient.upload( + 1002, + fs.createReadStream(video.thumbPath!, {highWaterMark: BlockSize}), + +video.thumbSize!, + md5, + extend + ); + } else { + this.logger.logDebug(`[Highway] uploadC2CVideoReq get upload invalid thumb ukey ${subFile.uKey}, don't need upload!`); + } + video.msgInfo = preRespData.upload.msgInfo; + } + + private async uploadGroupPttReq(groupUin: number, ptt: PacketMsgPttElement): Promise { + ptt.fileSha1 = Buffer.from(await calculateSha1(ptt.filePath)).toString('hex'); + const preReq = await this.packer.packUploadGroupPttReq(groupUin, ptt); + const preRespRaw = await this.packetClient.sendOidbPacket(preReq, true); + const preResp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode( + Buffer.from(preRespRaw.hex_data, 'hex') + ); + const preRespData = new NapProtoMsg(NTV2RichMediaResp).decode(preResp.body); + const ukey = preRespData.upload.uKey; + if (ukey && ukey != "") { + this.logger.logDebug(`[Highway] uploadGroupPttReq get upload ptt ukey: ${ukey}, need upload!`); + const index = preRespData.upload.msgInfo.msgInfoBody[0].index; + const md5 = Buffer.from(index.info.fileHash, 'hex'); + const sha1 = Buffer.from(index.info.fileSha1, 'hex'); + const extend = new NapProtoMsg(NTV2RichMediaHighwayExt).encode({ + fileUuid: index.fileUuid, + uKey: ukey, + network: { + ipv4S: oidbIpv4s2HighwayIpv4s(preRespData.upload.ipv4S) + }, + msgInfoBody: preRespData.upload.msgInfo.msgInfoBody, + blockSize: BlockSize, + hash: { + fileSha1: [sha1] + } + }) + await this.packetHighwayClient.upload( + 1008, + fs.createReadStream(ptt.filePath, {highWaterMark: BlockSize}), + ptt.fileSize, + md5, + extend + ); + } else { + this.logger.logDebug(`[Highway] uploadGroupPttReq get upload invalid ukey ${ukey}, don't need upload!`); + } + ptt.msgInfo = preRespData.upload.msgInfo; + } + + private async uploadC2CPttReq(peerUid: string, ptt: PacketMsgPttElement): Promise { + ptt.fileSha1 = Buffer.from(await calculateSha1(ptt.filePath)).toString('hex'); + const preReq = await this.packer.packUploadC2CPttReq(peerUid, ptt); + const preRespRaw = await this.packetClient.sendOidbPacket(preReq, true); + const preResp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode( + Buffer.from(preRespRaw.hex_data, 'hex') + ); + const preRespData = new NapProtoMsg(NTV2RichMediaResp).decode(preResp.body); + const ukey = preRespData.upload.uKey; + if (ukey && ukey != "") { + this.logger.logDebug(`[Highway] uploadC2CPttReq get upload ptt ukey: ${ukey}, need upload!`); + const index = preRespData.upload.msgInfo.msgInfoBody[0].index; + const md5 = Buffer.from(index.info.fileHash, 'hex'); + const sha1 = Buffer.from(index.info.fileSha1, 'hex'); + const extend = new NapProtoMsg(NTV2RichMediaHighwayExt).encode({ + fileUuid: index.fileUuid, + uKey: ukey, + network: { + ipv4S: oidbIpv4s2HighwayIpv4s(preRespData.upload.ipv4S) + }, + msgInfoBody: preRespData.upload.msgInfo.msgInfoBody, + blockSize: BlockSize, + hash: { + fileSha1: [sha1] + } + }) + await this.packetHighwayClient.upload( + 1007, + fs.createReadStream(ptt.filePath, {highWaterMark: BlockSize}), + ptt.fileSize, + md5, + extend + ); + } else { + this.logger.logDebug(`[Highway] uploadC2CPttReq get upload invalid ukey ${ukey}, don't need upload!`); + } + ptt.msgInfo = preRespData.upload.msgInfo; + } + + private async uploadGroupFileReq(groupUin: number, file: PacketMsgFileElement): Promise { + file.isGroupFile = true; + file.fileMd5 = await computeMd5AndLengthWithLimit(file.filePath); + file.fileSha1 = await calculateSha1(file.filePath); + const preReq = await this.packer.packUploadGroupFileReq(groupUin, file); + const preRespRaw = await this.packetClient.sendOidbPacket(preReq, true); + const preResp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode( + Buffer.from(preRespRaw.hex_data, 'hex') + ); + const preRespData = new NapProtoMsg(OidbSvcTrpcTcp0x6D6Response).decode(preResp.body); + if (!preRespData?.upload?.boolFileExist) { + this.logger.logDebug(`[Highway] uploadGroupFileReq file not exist, need upload!`); + const ext = new NapProtoMsg(FileUploadExt).encode({ + unknown1: 100, + unknown2: 1, + entry: { + busiBuff: { + senderUin: BigInt(this.sig.uin), + receiverUin: BigInt(groupUin), + groupCode: BigInt(groupUin), + }, + fileEntry: { + fileSize: BigInt(file.fileSize), + md5: file.fileMd5, + md5S2: file.fileMd5, + checkKey: preRespData.upload.checkKey, + fileId: preRespData.upload.fileId, + uploadKey: preRespData.upload.fileKey, + }, + clientInfo: { + clientType: 3, + appId: "100", + terminalType: 3, + clientVer: "1.1.1", + unknown: 4 + }, + fileNameInfo: { + fileName: file.fileName + }, + host: { + hosts: [ + { + url: { + host: preRespData.upload.uploadIp, + unknown: 1, + }, + port: preRespData.upload.uploadPort, + } + ] + } + }, + unknown200: 0, + }) + await this.packetHighwayClient.upload( + 71, + fs.createReadStream(file.filePath, {highWaterMark: BlockSize}), + file.fileSize, + file.fileMd5, + ext + ); + } else { + this.logger.logDebug(`[Highway] uploadGroupFileReq file exist, don't need upload!`); + } + file.fileUuid = preRespData.upload.fileId; + } + + private async uploadC2CFileReq(peerUid: string, file: PacketMsgFileElement): Promise { + file.isGroupFile = false; + file.fileMd5 = await computeMd5AndLengthWithLimit(file.filePath); + file.fileSha1 = await calculateSha1(file.filePath); + const preReq = await this.packer.packUploadC2CFileReq(this.sig.uid, peerUid, file); + const preRespRaw = await this.packetClient.sendOidbPacket( preReq, true); + const preResp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode( + Buffer.from(preRespRaw.hex_data, 'hex') + ); + const preRespData = new NapProtoMsg(OidbSvcTrpcTcp0XE37Response).decode(preResp.body); + if (!preRespData.upload?.boolFileExist) { + this.logger.logDebug(`[Highway] uploadC2CFileReq file not exist, need upload!`); + const ext = new NapProtoMsg(FileUploadExt).encode({ + unknown1: 100, + unknown2: 1, + entry: { + busiBuff: { + senderUin: BigInt(this.sig.uin), + }, + fileEntry: { + fileSize: BigInt(file.fileSize), + md5: file.fileMd5, + md5S2: file.fileMd5, + checkKey: file.fileSha1, + fileId: preRespData.upload?.uuid, + uploadKey: preRespData.upload?.mediaPlatformUploadKey, + }, + clientInfo: { + clientType: 3, + appId: "100", + terminalType: 3, + clientVer: "1.1.1", + unknown: 4 + }, + fileNameInfo: { + fileName: file.fileName + }, + host: { + hosts: [ + { + url: { + host: preRespData.upload?.uploadIp, + unknown: 1, + }, + port: preRespData.upload?.uploadPort, + } + ] + } + }, + unknown200: 1, + unknown3: 0 + }) + await this.packetHighwayClient.upload( + 95, + fs.createReadStream(file.filePath, {highWaterMark: BlockSize}), + file.fileSize, + file.fileMd5, + ext + ); + } + file.fileUuid = preRespData.upload?.uuid; + file.fileHash = preRespData.upload?.fileAddon; + const FetchExistFileReq = this.packer.packOfflineFileDownloadReq(file.fileUuid!, file.fileHash!, this.sig.uid, peerUid); + const resp = await this.packetClient.sendOidbPacket(FetchExistFileReq, true); + const oidb_resp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(Buffer.from(resp.hex_data, 'hex')); + file._e37_800_rsp = new NapProtoMsg(OidbSvcTrpcTcp0XE37_800Response).decode(oidb_resp.body); + file._private_send_uid = this.sig.uid; + file._private_recv_uid = peerUid; + } } diff --git a/src/core/packet/highway/uploader.ts b/src/core/packet/highway/uploader.ts index 4b4a864f..43acaa1e 100644 --- a/src/core/packet/highway/uploader.ts +++ b/src/core/packet/highway/uploader.ts @@ -19,11 +19,20 @@ abstract class HighwayUploader { this.logger = logger; } - encryptTransExt(key: Uint8Array) { + private encryptTransExt(key: Uint8Array) { if (!this.trans.encrypt) return; this.trans.ext = tea.encrypt(Buffer.from(this.trans.ext), Buffer.from(key)); } + protected timeout(): Promise { + return new Promise((_, reject) => { + setTimeout(() => { + reject(new Error(`[Highway] timeout after ${this.trans.timeout}s`)); + }, (this.trans.timeout ?? Infinity) * 1000 + ); + }) + } + buildPicUpHead(offset: number, bodyLength: number, bodyMd5: Uint8Array): Uint8Array { return new NapProtoMsg(ReqDataHighwayHead).encode({ msgBaseHead: { @@ -86,15 +95,18 @@ class HighwayTcpUploaderTransform extends stream.Transform { export class HighwayTcpUploader extends HighwayUploader { async upload(): Promise { - const highwayTransForm = new HighwayTcpUploaderTransform(this); - const upload = new Promise((resolve, _) => { + const controller = new AbortController(); + const { signal } = controller; + const upload = new Promise((resolve, reject) => { + const highwayTransForm = new HighwayTcpUploaderTransform(this); const socket = net.connect(this.trans.port, this.trans.server, () => { - this.trans.data.pipe(highwayTransForm).pipe(socket, { end: false }); + this.trans.data.pipe(highwayTransForm).pipe(socket, {end: false}); }); const handleRspHeader = (header: Buffer) => { const rsp = new NapProtoMsg(RespDataHighwayHead).decode(header); if (rsp.errorCode !== 0) { - this.logger.logWarn(`[Highway] tcpUpload failed (code: ${rsp.errorCode})`); + socket.end(); + reject(new Error(`[Highway] tcpUpload failed (code=${rsp.errorCode})`)); } const percent = ((Number(rsp.msgSegHead?.dataOffset) + Number(rsp.msgSegHead?.dataLength)) / Number(rsp.msgSegHead?.filesize)).toFixed(2); this.logger.logDebug(`[Highway] tcpUpload ${rsp.errorCode} | ${percent} | ${Buffer.from(header).toString('hex')}`); @@ -105,49 +117,58 @@ export class HighwayTcpUploader extends HighwayUploader { } }; socket.on('data', (chunk: Buffer) => { - try { - const [head, _] = Frame.unpack(chunk); - handleRspHeader(head); - } catch (e) { - this.logger.logError(`[Highway] tcpUpload parse response error: ${e}`); + if (signal.aborted) { + socket.end(); + reject(new Error('Upload aborted due to timeout')); } + const [head, _] = Frame.unpack(chunk); + handleRspHeader(head); }); socket.on('close', () => { this.logger.logDebug('[Highway] tcpUpload socket closed.'); resolve(); }); socket.on('error', (err) => { - this.logger.logError('[Highway] tcpUpload socket.on error:', err); + socket.end(); + reject(new Error(`[Highway] tcpUpload socket.on error: ${err}`)); }); this.trans.data.on('error', (err) => { - this.logger.logError('[Highway] tcpUpload readable error:', err); socket.end(); + reject(new Error(`[Highway] tcpUpload readable error: ${err}`)); }); }); - const timeout = new Promise((_, reject) => { - setTimeout(() => { - reject(new Error(`[Highway] tcpUpload timeout after ${this.trans.timeout}s`)); - }, (this.trans.timeout ?? Infinity) * 1000 - ); + const timeout = this.timeout().catch((err) => { + controller.abort(); + throw new Error(err.message); }); await Promise.race([upload, timeout]); } } -// TODO: timeout impl export class HighwayHttpUploader extends HighwayUploader { async upload(): Promise { - let offset = 0; - for await (const chunk of this.trans.data) { - const block = chunk as Buffer; - try { - await this.uploadBlock(block, offset); - } catch (err) { - this.logger.logError(`[Highway] httpUpload Error uploading block at offset ${offset}: ${err}`); - throw err; + const controller = new AbortController(); + const { signal } = controller; + const upload = (async () => { + let offset = 0; + for await (const chunk of this.trans.data) { + if (signal.aborted) { + throw new Error('Upload aborted due to timeout'); + } + const block = chunk as Buffer; + try { + await this.uploadBlock(block, offset); + } catch (err) { + throw new Error(`[Highway] httpUpload Error uploading block at offset ${offset}: ${err}`) + } + offset += block.length; } - offset += block.length; - } + })(); + const timeout = this.timeout().catch((err) => { + controller.abort(); + throw new Error(err.message); + }); + await Promise.race([upload, timeout]); } private async uploadBlock(block: Buffer, offset: number): Promise { @@ -158,9 +179,7 @@ export class HighwayHttpUploader extends HighwayUploader { const [head, body] = Frame.unpack(resp); const headData = new NapProtoMsg(RespDataHighwayHead).decode(head); this.logger.logDebug(`[Highway] httpUploadBlock: ${headData.errorCode} | ${headData.msgSegHead?.retCode} | ${headData.bytesRspExtendInfo} | ${head.toString('hex')} | ${body.toString('hex')}`); - if (headData.errorCode !== 0) { - this.logger.logError(`[Highway] httpUploadBlock failed (code=${headData.errorCode})`); - } + if (headData.errorCode !== 0) throw new Error(`[Highway] httpUploadBlock failed (code=${headData.errorCode})`); } private async httpPostHighwayContent(frame: Buffer, serverURL: string): Promise { @@ -176,12 +195,12 @@ export class HighwayHttpUploader extends HighwayUploader { }, }; const req = http.request(serverURL, options, (res) => { - let data = Buffer.alloc(0); + const data: Buffer[] = []; res.on('data', (chunk) => { - data = Buffer.concat([data, chunk]); + data.push(chunk); }); res.on('end', () => { - resolve(data); + resolve(Buffer.concat(data)); }); }); req.write(frame); diff --git a/src/core/packet/msg/builder.ts b/src/core/packet/msg/builder.ts index 97736ae2..eef14eec 100644 --- a/src/core/packet/msg/builder.ts +++ b/src/core/packet/msg/builder.ts @@ -2,7 +2,9 @@ import * as crypto from "crypto"; import { PushMsgBody } from "@/core/packet/proto/message/message"; import { NapProtoEncodeStructType } from "@/core/packet/proto/NapProto"; import { LogWrapper } from "@/common/log"; -import { PacketMsg } from "@/core/packet/msg/message"; +import { PacketMsg, PacketSendMsgElement } from "@/core/packet/msg/message"; +import { IPacketMsgElement, PacketMsgTextElement } from "@/core/packet/msg/element"; +import { SendTextElement } from "@/core"; export class PacketMsgBuilder { private logger: LogWrapper; @@ -11,10 +13,23 @@ export class PacketMsgBuilder { this.logger = logger; } + protected static failBackText = new PacketMsgTextElement( + { + textElement: {content: "[该消息类型暂不支持查看]"}! + } as SendTextElement + ) + buildFakeMsg(selfUid: string, element: PacketMsg[]): NapProtoEncodeStructType[] { return element.map((node): NapProtoEncodeStructType => { const avatar = `https://q.qlogo.cn/headimg_dl?dst_uin=${node.senderUin}&spec=640&img_type=jpg`; + const msgContent = node.msg.reduceRight((acc: undefined | Uint8Array, msg: IPacketMsgElement) => { + return acc !== undefined ? acc : msg.buildContent(); + }, undefined); const msgElement = node.msg.flatMap(msg => msg.buildElement() ?? []); + if (!msgContent && !msgElement.length) { + this.logger.logWarn(`[PacketMsgBuilder] buildFakeMsg: 空的msgContent和msgElement!`); + msgElement.push(PacketMsgBuilder.failBackText.buildElement()); + } return { responseHead: { fromUid: "", @@ -50,7 +65,8 @@ export class PacketMsgBuilder { body: { richText: { elems: msgElement - } + }, + msgContent: msgContent, } }; }); diff --git a/src/core/packet/msg/element.ts b/src/core/packet/msg/element.ts index 81d1401f..5996e93c 100644 --- a/src/core/packet/msg/element.ts +++ b/src/core/packet/msg/element.ts @@ -1,4 +1,3 @@ -import assert from "node:assert"; import * as zlib from "node:zlib"; import { NapProtoEncodeStructType, NapProtoMsg } from "@/core/packet/proto/NapProto"; import { @@ -28,6 +27,8 @@ import { import { MsgInfo } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaReq"; import { PacketMsg, PacketSendMsgElement } from "@/core/packet/msg/message"; import { ForwardMsgBuilder } from "@/common/forward-msg-builder"; +import { FileExtra, GroupFileExtra } from "@/core/packet/proto/message/component"; +import { OidbSvcTrpcTcp0XE37_800Response } from "@/core/packet/proto/oidb/Oidb.0XE37_800"; // raw <-> packet // TODO: SendStructLongMsgElement @@ -35,16 +36,20 @@ export abstract class IPacketMsgElement { protected constructor(rawElement: T) { } + get valid(): boolean { + return true; + } + buildContent(): Uint8Array | undefined { return undefined; } - buildElement(): NapProtoEncodeStructType[] | undefined { - return undefined; + buildElement(): NapProtoEncodeStructType[] { + return []; } toPreview(): string { - return '[nya~]'; + return '[暂不支持该消息类型喵~]'; } } @@ -84,59 +89,15 @@ export class PacketMsgAtElement extends PacketMsgTextElement { text: { str: this.text, pbReserve: new NapProtoMsg(MentionExtra).encode({ - type: this.atAll ? 1 : 2, - uin: 0, - field5: 0, - uid: this.targetUid, - } + type: this.atAll ? 1 : 2, + uin: 0, + field5: 0, + uid: this.targetUid, + } ) } }]; } - - toPreview(): string { - return `@${this.targetUid} ${this.text}`; - } -} - -export class PacketMsgPicElement extends IPacketMsgElement { - path: string; - name: string; - size: number; - md5: string; - width: number; - height: number; - picType: PicType; - sha1: string | null = null; - msgInfo: NapProtoEncodeStructType | null = null; - groupPicExt: NapProtoEncodeStructType | null = null; - c2cPicExt: NapProtoEncodeStructType | null = null; - - constructor(element: SendPicElement) { - super(element); - this.path = element.picElement.sourcePath; - this.name = element.picElement.fileName; - this.size = Number(element.picElement.fileSize); - this.md5 = element.picElement.md5HexStr ?? ''; - this.width = element.picElement.picWidth; - this.height = element.picElement.picHeight; - this.picType = element.picElement.picType; - } - - buildElement(): NapProtoEncodeStructType[] { - assert(this.msgInfo !== null, 'msgInfo is null, expected not null'); - return [{ - commonElem: { - serviceType: 48, - pbElem: new NapProtoMsg(MsgInfo).encode(this.msgInfo), - businessType: 10, - } - }]; - } - - toPreview(): string { - return "[图片]"; - } } export class PacketMsgReplyElement extends IPacketMsgElement { @@ -151,11 +112,11 @@ export class PacketMsgReplyElement extends IPacketMsgElement { constructor(element: SendReplyElement) { super(element); this.messageId = BigInt(element.replyElement.replayMsgId ?? 0); - this.messageSeq = Number(element.replyElement.replayMsgSeq ?? 0); - this.messageClientSeq = Number(element.replyElement.replyMsgClientSeq ?? 0); - this.targetUin = Number(element.replyElement.senderUin ?? 0); + this.messageSeq = +(element.replyElement.replayMsgSeq ?? 0); + this.messageClientSeq = +(element.replyElement.replyMsgClientSeq ?? 0); + this.targetUin = +(element.replyElement.senderUin ?? 0); this.targetUid = element.replyElement.senderUidStr ?? ''; - this.time = Number(element.replyElement.replyMsgTime ?? 0); + this.time = +(element.replyElement.replyMsgTime ?? 0); this.elems = []; // TODO: in replyElement.sourceMsgTextElems } @@ -189,7 +150,7 @@ export class PacketMsgReplyElement extends IPacketMsgElement { } toPreview(): string { - return "[回复]"; + return "[回复消息]"; } } @@ -284,21 +245,216 @@ export class PacketMsgMarkFaceElement extends IPacketMsgElement { - constructor(element: SendVideoElement) { +export class PacketMsgPicElement extends IPacketMsgElement { + path: string; + name: string; + size: number; + md5: string; + width: number; + height: number; + picType: PicType; + sha1: string | null = null; + msgInfo: NapProtoEncodeStructType | null = null; + groupPicExt: NapProtoEncodeStructType | null = null; + c2cPicExt: NapProtoEncodeStructType | null = null; + + constructor(element: SendPicElement) { super(element); + this.path = element.picElement.sourcePath; + this.name = element.picElement.fileName; + this.size = +element.picElement.fileSize; + this.md5 = element.picElement.md5HexStr ?? ''; + this.width = element.picElement.picWidth; + this.height = element.picElement.picHeight; + this.picType = element.picElement.picType; + } + + get valid(): boolean { + return !!this.msgInfo; + } + + buildElement(): NapProtoEncodeStructType[] { + if (!this.msgInfo) return []; + return [{ + commonElem: { + serviceType: 48, + pbElem: new NapProtoMsg(MsgInfo).encode(this.msgInfo), + businessType: 10, + } + }]; + } + + toPreview(): string { + return "[图片]"; } } -export class PacketMsgFileElement extends IPacketMsgElement { - constructor(element: SendFileElement) { +export class PacketMsgVideoElement extends IPacketMsgElement { + fileSize?: string; + filePath?: string; + thumbSize?: number; + thumbPath?: string; + fileMd5?: string; + fileSha1?: string; + thumbMd5?: string; + thumbSha1?: string; + thumbWidth?: number; + thumbHeight?: number; + msgInfo: NapProtoEncodeStructType | null = null; + + constructor(element: SendVideoElement) { super(element); + this.fileSize = element.videoElement.fileSize; + this.filePath = element.videoElement.filePath; + this.thumbSize = element.videoElement.thumbSize; + this.thumbPath = element.videoElement.thumbPath?.get(0); + this.fileMd5 = element.videoElement.videoMd5 + this.thumbMd5 = element.videoElement.thumbMd5; + this.thumbWidth = element.videoElement.thumbWidth; + this.thumbHeight = element.videoElement.thumbHeight; + } + + get valid(): boolean { + return !!this.msgInfo; + } + + buildElement(): NapProtoEncodeStructType[] { + if (!this.msgInfo) return []; + return [{ + commonElem: { + serviceType: 48, + pbElem: new NapProtoMsg(MsgInfo).encode(this.msgInfo), + businessType: 21, + } + }]; + } + + toPreview(): string { + return "[视频]"; } } export class PacketMsgPttElement extends IPacketMsgElement { + filePath: string; + fileSize: number; + fileMd5: string; + fileSha1?: string; + fileDuration: number; + msgInfo: NapProtoEncodeStructType | null = null; + constructor(element: SendPttElement) { super(element); + this.filePath = element.pttElement.filePath; + this.fileSize = +element.pttElement.fileSize; // TODO: cc + this.fileMd5 = element.pttElement.md5HexStr; + this.fileDuration = Math.round(element.pttElement.duration); // TODO: cc + } + + get valid(): boolean { + return false; + } + + buildElement(): NapProtoEncodeStructType[] { + return [] + // if (!this.msgInfo) return []; + // return [{ + // commonElem: { + // serviceType: 48, + // pbElem: new NapProtoMsg(MsgInfo).encode(this.msgInfo), + // businessType: 22, + // } + // }]; + } + + toPreview(): string { + return "[语音]"; + } +} + +export class PacketMsgFileElement extends IPacketMsgElement { + fileName: string; + filePath: string; + fileSize: number; + fileSha1?: Uint8Array; + fileMd5?: Uint8Array; + fileUuid?: string; + fileHash?: string; + isGroupFile?: boolean; + _private_send_uid?: string; + _private_recv_uid?: string; + _e37_800_rsp?: NapProtoEncodeStructType + + constructor(element: SendFileElement) { + super(element); + this.fileName = element.fileElement.fileName; + this.filePath = element.fileElement.filePath; + this.fileSize = +element.fileElement.fileSize; + } + + get valid(): boolean { + return this.isGroupFile || Boolean(this._e37_800_rsp); + } + + buildContent(): Uint8Array | undefined { + if (this.isGroupFile || !this._e37_800_rsp) return undefined; + return new NapProtoMsg(FileExtra).encode({ + file: { + fileType: 0, + fileUuid: this.fileUuid, + fileMd5: this.fileMd5, + fileName: this.fileName, + fileSize: BigInt(this.fileSize), + subcmd: 1, + dangerEvel: 0, + expireTime: Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60, + fileHash: this.fileHash, + }, + field6: { + field2: { + field1: this._e37_800_rsp?.body?.field30?.field110, + fileUuid: this.fileUuid, + fileName: this.fileName, + field6: this._e37_800_rsp?.body?.field30?.field3, + field7: this._e37_800_rsp?.body?.field30?.field101, + field8: this._e37_800_rsp?.body?.field30?.field100, + timestamp1: this._e37_800_rsp?.body?.field30?.timestamp1, + fileHash: this.fileHash, + selfUid: this._private_send_uid, + destUid: this._private_recv_uid, + } + } + }) + } + + buildElement(): NapProtoEncodeStructType[] { + if (!this.isGroupFile) return []; + const lb = Buffer.alloc(2); + const transElemVal = new NapProtoMsg(GroupFileExtra).encode({ + field1: 6, + fileName: this.fileName, + inner: { + info: { + busId: 102, + fileId: this.fileUuid, + fileSize: BigInt(this.fileSize), + fileName: this.fileName, + fileSha: this.fileSha1, + extInfoString: "", + fileMd5: this.fileMd5, + } + } + }) + lb.writeUInt16BE(transElemVal.length); + return [{ + transElem: { + elemType: 24, + elemValue: Buffer.concat([Buffer.from([0x01]), lb, transElemVal]) // TLV + } + }]; + } + + toPreview(): string { + return `[文件]${this.fileName}`; } } diff --git a/src/core/packet/packer.ts b/src/core/packet/packer.ts index cb53db5c..95a37a53 100644 --- a/src/core/packet/packer.ts +++ b/src/core/packet/packer.ts @@ -1,6 +1,6 @@ import * as zlib from "node:zlib"; import * as crypto from "node:crypto"; -import { calculateSha1 } from "@/core/packet/utils/crypto/hash"; +import { computeMd5AndLengthWithLimit } from "@/core/packet/utils/crypto/hash"; import { NapProtoMsg } from "@/core/packet/proto/NapProto"; import { OidbSvcTrpcTcpBase } from "@/core/packet/proto/oidb/OidbBase"; import { OidbSvcTrpcTcp0X9067_202 } from "@/core/packet/proto/oidb/Oidb.0x9067_202"; @@ -11,16 +11,29 @@ import { NTV2RichMediaReq } from "@/core/packet/proto/oidb/common/Ntv2.RichMedia import { HttpConn0x6ff_501 } from "@/core/packet/proto/action/action"; import { LongMsgResult, SendLongMsgReq } from "@/core/packet/proto/message/action"; import { PacketMsgBuilder } from "@/core/packet/msg/builder"; -import { PacketMsgPicElement } from "@/core/packet/msg/element"; +import { + PacketMsgFileElement, + PacketMsgPicElement, + PacketMsgPttElement, + PacketMsgVideoElement +} from "@/core/packet/msg/element"; import { LogWrapper } from "@/common/log"; import { PacketMsg } from "@/core/packet/msg/message"; import { OidbSvcTrpcTcp0x6D6 } from "@/core/packet/proto/oidb/Oidb.0x6D6"; import { OidbSvcTrpcTcp0XE37_1200 } from "@/core/packet/proto/oidb/Oidb.0xE37_1200"; import { PacketMsgConverter } from "@/core/packet/msg/converter"; import { PacketClient } from "@/core/packet/client"; +import { OidbSvcTrpcTcp0XE37_1700 } from "@/core/packet/proto/oidb/Oidb.0xE37_1700"; +import { OidbSvcTrpcTcp0XE37_800 } from "@/core/packet/proto/oidb/Oidb.0XE37_800"; +import { OidbSvcTrpcTcp0XEB7 } from "./proto/oidb/Oidb.0xEB7"; export type PacketHexStr = string & { readonly hexNya: unique symbol }; +export interface OidbPacket { + cmd: string; + data: PacketHexStr +} + export class PacketPacker { readonly logger: LogWrapper; readonly client: PacketClient; @@ -34,30 +47,34 @@ export class PacketPacker { this.packetConverter = new PacketMsgConverter(logger); } - private toHexStr(byteArray: Uint8Array): PacketHexStr { + private packetPacket(byteArray: Uint8Array): PacketHexStr { return Buffer.from(byteArray).toString('hex') as PacketHexStr; } - packOidbPacket(cmd: number, subCmd: number, body: Uint8Array, isUid: boolean = true, isLafter: boolean = false): Uint8Array { - return new NapProtoMsg(OidbSvcTrpcTcpBase).encode({ + packOidbPacket(cmd: number, subCmd: number, body: Uint8Array, isUid: boolean = true, isLafter: boolean = false): OidbPacket { + const data = new NapProtoMsg(OidbSvcTrpcTcpBase).encode({ command: cmd, subCommand: subCmd, body: body, isReserved: isUid ? 1 : 0 }); + return { + cmd: `OidbSvcTrpcTcp.0x${cmd.toString(16).toUpperCase()}_${subCmd}`, + data: this.packetPacket(data) + } } - packPokePacket(peer: number, group?: number): PacketHexStr { + packPokePacket(peer: number, group?: number): OidbPacket { const oidb_0xed3 = new NapProtoMsg(OidbSvcTrpcTcp0XED3_1).encode({ uin: peer, groupUin: group, friendUin: group ?? peer, ext: 0 }); - return this.toHexStr(this.packOidbPacket(0xed3, 1, oidb_0xed3)); + return this.packOidbPacket(0xed3, 1, oidb_0xed3); } - packRkeyPacket(): PacketHexStr { + packRkeyPacket(): OidbPacket { const oidb_0x9067_202 = new NapProtoMsg(OidbSvcTrpcTcp0X9067_202).encode({ reqHead: { common: { @@ -77,10 +94,10 @@ export class PacketPacker { key: [10, 20, 2] }, }); - return this.toHexStr(this.packOidbPacket(0x9067, 202, oidb_0x9067_202)); + return this.packOidbPacket(0x9067, 202, oidb_0x9067_202); } - packSetSpecialTittlePacket(groupCode: string, uid: string, tittle: string): PacketHexStr { + packSetSpecialTittlePacket(groupCode: string, uid: string, tittle: string): OidbPacket { const oidb_0x8FC_2_body = new NapProtoMsg(OidbSvcTrpcTcp0X8FC_2_Body).encode({ targetUid: uid, specialTitle: tittle, @@ -91,15 +108,15 @@ export class PacketPacker { groupUin: +groupCode, body: oidb_0x8FC_2_body }); - return this.toHexStr(this.packOidbPacket(0x8FC, 2, oidb_0x8FC_2, false, false)); + return this.packOidbPacket(0x8FC, 2, oidb_0x8FC_2, false, false); } - packStatusPacket(uin: number): PacketHexStr { + packStatusPacket(uin: number): OidbPacket { const oidb_0xfe1_2 = new NapProtoMsg(OidbSvcTrpcTcp0XFE1_2).encode({ uin: uin, - key: [{ key: 27372 }] + key: [{key: 27372}] }); - return this.toHexStr(this.packOidbPacket(0xfe1, 2, oidb_0xfe1_2)); + return this.packOidbPacket(0xfe1, 2, oidb_0xfe1_2); } async packUploadForwardMsg(selfUid: string, msg: PacketMsg[], groupUin: number = 0): Promise { @@ -131,12 +148,12 @@ export class PacketPacker { } ); // this.logger.logDebug("packUploadForwardMsg REQ!!!", req); - return this.toHexStr(req); + return this.packetPacket(req); } // highway part packHttp0x6ff_501(): PacketHexStr { - return this.toHexStr(new NapProtoMsg(HttpConn0x6ff_501).encode({ + return this.packetPacket(new NapProtoMsg(HttpConn0x6ff_501).encode({ httpConn: { field1: 0, field2: 0, @@ -153,7 +170,7 @@ export class PacketPacker { })); } - async packUploadGroupImgReq(groupUin: number, img: PacketMsgPicElement): Promise { + async packUploadGroupImgReq(groupUin: number, img: PacketMsgPicElement): Promise { const req = new NapProtoMsg(NTV2RichMediaReq).encode( { reqHead: { @@ -177,9 +194,9 @@ export class PacketPacker { uploadInfo: [ { fileInfo: { - fileSize: Number(img.size), + fileSize: +img.size, fileHash: img.md5, - fileSha1: this.toHexStr(await calculateSha1(img.path)), + fileSha1: img.sha1!, fileName: img.name, type: { type: 1, @@ -218,62 +235,148 @@ export class PacketPacker { } } ); - return this.toHexStr(this.packOidbPacket(0x11c4, 100, req, true, false)); + return this.packOidbPacket(0x11c4, 100, req, true, false); } - async packUploadC2CImgReq(peerUin: string, img: PacketMsgPicElement): Promise { + async packUploadC2CImgReq(peerUin: string, img: PacketMsgPicElement): Promise { + const req = new NapProtoMsg(NTV2RichMediaReq).encode({ + reqHead: { + common: { + requestId: 1, + command: 100 + }, + scene: { + requestType: 2, + businessType: 1, + sceneType: 1, + c2C: { + accountType: 2, + targetUid: peerUin + }, + }, + client: { + agentType: 2, + } + }, + upload: { + uploadInfo: [ + { + fileInfo: { + fileSize: +img.size, + fileHash: img.md5, + fileSha1: img.sha1!, + fileName: img.name, + type: { + type: 1, + picFormat: img.picType, //TODO: extend NapCat imgType /cc @MliKiowa + videoFormat: 0, + voiceFormat: 0, + }, + width: img.width, + height: img.height, + time: 0, + original: 1 + }, + subFileType: 0, + } + ], + tryFastUploadCompleted: true, + srvSendMsg: false, + clientRandomId: crypto.randomBytes(8).readBigUInt64BE() & BigInt('0x7FFFFFFFFFFFFFFF'), + compatQMsgSceneType: 1, + extBizInfo: { + pic: { + bytesPbReserveTroop: Buffer.from("0800180020004200500062009201009a0100a2010c080012001800200028003a00", 'hex'), + textSummary: "Nya~", // TODO: + }, + video: { + bytesPbReserve: Buffer.alloc(0), + }, + ptt: { + bytesPbReserve: Buffer.alloc(0), + bytesReserve: Buffer.alloc(0), + bytesGeneralFlags: Buffer.alloc(0), + } + }, + clientSeq: 0, + noNeedCompatMsg: false, + } + } + ); + return this.packOidbPacket(0x11c5, 100, req, true, false); + } + + async packUploadGroupVideoReq(groupUin: number, video: PacketMsgVideoElement): Promise { + if (!video.fileSize || !video.thumbSize) throw new Error("video.fileSize or video.thumbSize is empty"); const req = new NapProtoMsg(NTV2RichMediaReq).encode({ reqHead: { common: { - requestId: 1, + requestId: 3, command: 100 }, scene: { requestType: 2, - businessType: 1, - sceneType: 1, - c2C: { - accountType: 2, - targetUid: peerUin + businessType: 2, + sceneType: 2, + group: { + groupUin: groupUin }, }, client: { - agentType: 2, + agentType: 2 } }, upload: { uploadInfo: [ { fileInfo: { - fileSize: Number(img.size), - fileHash: img.md5, - fileSha1: this.toHexStr(await calculateSha1(img.path)), - fileName: img.name, + fileSize: +video.fileSize, + fileHash: video.fileMd5, + fileSha1: video.fileSha1, + fileName: "nya.mp4", + type: { + type: 2, + picFormat: 0, + videoFormat: 0, + voiceFormat: 0 + }, + height: 0, + width: 0, + time: 0, + original: 0 + }, + subFileType: 0 + }, { + fileInfo: { + fileSize: +video.thumbSize, + fileHash: video.thumbMd5, + fileSha1: video.thumbSha1, + fileName: "nya.jpg", type: { type: 1, - picFormat: img.picType, //TODO: extend NapCat imgType /cc @MliKiowa + picFormat: 0, videoFormat: 0, - voiceFormat: 0, + voiceFormat: 0 }, - width: img.width, - height: img.height, + height: video.thumbHeight, + width: video.thumbWidth, time: 0, - original: 1 + original: 0 }, - subFileType: 0, + subFileType: 100 } ], tryFastUploadCompleted: true, srvSendMsg: false, clientRandomId: crypto.randomBytes(8).readBigUInt64BE() & BigInt('0x7FFFFFFFFFFFFFFF'), - compatQMsgSceneType: 1, + compatQMsgSceneType: 2, extBizInfo: { pic: { - bytesPbReserveTroop: Buffer.from("0800180020004200500062009201009a0100a2010c080012001800200028003a00", 'hex'), - textSummary: "Nya~", // TODO: + bizType: 0, + textSummary: "Nya~", }, video: { - bytesPbReserve: Buffer.alloc(0), + bytesPbReserve: Buffer.from([0x80, 0x01, 0x00]), }, ptt: { bytesPbReserve: Buffer.alloc(0), @@ -282,28 +385,297 @@ export class PacketPacker { } }, clientSeq: 0, - noNeedCompatMsg: false, + noNeedCompatMsg: false } - } - ); - return this.toHexStr(this.packOidbPacket(0x11c5, 100, req, true, false)); + }); + return this.packOidbPacket(0x11EA, 100, req, true, false); } - packGroupFileDownloadReq(groupUin: number, fileUUID: string): PacketHexStr { - return this.toHexStr( - this.packOidbPacket(0x6D6, 2, new NapProtoMsg(OidbSvcTrpcTcp0x6D6).encode({ + async packUploadC2CVideoReq(peerUin: string, video: PacketMsgVideoElement): Promise { + if (!video.fileSize || !video.thumbSize) throw new Error("video.fileSize or video.thumbSize is empty"); + const req = new NapProtoMsg(NTV2RichMediaReq).encode({ + reqHead: { + common: { + requestId: 3, + command: 100 + }, + scene: { + requestType: 2, + businessType: 2, + sceneType: 1, + c2C: { + accountType: 2, + targetUid: peerUin + } + }, + client: { + agentType: 2 + } + }, + upload: { + uploadInfo: [ + { + fileInfo: { + fileSize: +video.fileSize, + fileHash: video.fileMd5, + fileSha1: video.fileSha1, + fileName: "nya.mp4", + type: { + type: 2, + picFormat: 0, + videoFormat: 0, + voiceFormat: 0 + }, + height: 0, + width: 0, + time: 0, + original: 0 + }, + subFileType: 0 + }, { + fileInfo: { + fileSize: +video.thumbSize, + fileHash: video.thumbMd5, + fileSha1: video.thumbSha1, + fileName: "nya.jpg", + type: { + type: 1, + picFormat: 0, + videoFormat: 0, + voiceFormat: 0 + }, + height: video.thumbHeight, + width: video.thumbWidth, + time: 0, + original: 0 + }, + subFileType: 100 + } + ], + tryFastUploadCompleted: true, + srvSendMsg: false, + clientRandomId: crypto.randomBytes(8).readBigUInt64BE() & BigInt('0x7FFFFFFFFFFFFFFF'), + compatQMsgSceneType: 2, + extBizInfo: { + pic: { + bizType: 0, + textSummary: "Nya~", + }, + video: { + bytesPbReserve: Buffer.from([0x80, 0x01, 0x00]), + }, + ptt: { + bytesPbReserve: Buffer.alloc(0), + bytesReserve: Buffer.alloc(0), + bytesGeneralFlags: Buffer.alloc(0), + } + }, + clientSeq: 0, + noNeedCompatMsg: false + } + }); + return this.packOidbPacket(0x11E9, 100, req, true, false); + } + + async packUploadGroupPttReq(groupUin: number, ptt: PacketMsgPttElement): Promise { + const req = new NapProtoMsg(NTV2RichMediaReq).encode({ + reqHead: { + common: { + requestId: 1, + command: 100 + }, + scene: { + requestType: 2, + businessType: 3, + sceneType: 2, + group: { + groupUin: groupUin + } + }, + client: { + agentType: 2 + } + }, + upload: { + uploadInfo: [ + { + fileInfo: { + fileSize: ptt.fileSize, + fileHash: ptt.fileMd5, + fileSha1: ptt.fileSha1, + fileName: `${ptt.fileMd5}.amr`, + type: { + type: 3, + picFormat: 0, + videoFormat: 0, + voiceFormat: 1 + }, + height: 0, + width: 0, + time: ptt.fileDuration, + original: 0 + }, + subFileType: 0 + } + ], + tryFastUploadCompleted: true, + srvSendMsg: false, + clientRandomId: crypto.randomBytes(8).readBigUInt64BE() & BigInt('0x7FFFFFFFFFFFFFFF'), + compatQMsgSceneType: 2, + extBizInfo: { + pic: { + textSummary: "Nya~", + }, + video: { + bytesPbReserve: Buffer.alloc(0), + }, + ptt: { + bytesPbReserve: Buffer.alloc(0), + bytesReserve: Buffer.from([0x08, 0x00, 0x38, 0x00]), + bytesGeneralFlags: Buffer.from([0x9a, 0x01, 0x07, 0xaa, 0x03, 0x04, 0x08, 0x08, 0x12, 0x00]), + } + }, + clientSeq: 0, + noNeedCompatMsg: false + } + }) + return this.packOidbPacket(0x126E, 100, req, true, false); + } + + async packUploadC2CPttReq(peerUin: string, ptt: PacketMsgPttElement): Promise { + const req = new NapProtoMsg(NTV2RichMediaReq).encode({ + reqHead: { + common: { + requestId: 4, + command: 100 + }, + scene: { + requestType: 2, + businessType: 3, + sceneType: 1, + c2C: { + accountType: 2, + targetUid: peerUin + } + }, + client: { + agentType: 2 + } + }, + upload: { + uploadInfo: [ + { + fileInfo: { + fileSize: ptt.fileSize, + fileHash: ptt.fileMd5, + fileSha1: ptt.fileSha1, + fileName: `${ptt.fileMd5}.amr`, + type: { + type: 3, + picFormat: 0, + videoFormat: 0, + voiceFormat: 1 + }, + height: 0, + width: 0, + time: ptt.fileDuration, + original: 0 + }, + subFileType: 0 + } + ], + tryFastUploadCompleted: true, + srvSendMsg: false, + clientRandomId: crypto.randomBytes(8).readBigUInt64BE() & BigInt('0x7FFFFFFFFFFFFFFF'), + compatQMsgSceneType: 1, + extBizInfo: { + pic: { + textSummary: "Nya~", + }, + ptt: { + bytesReserve: Buffer.from([0x08, 0x00, 0x38, 0x00]), + bytesGeneralFlags: Buffer.from([0x9a, 0x01, 0x0b, 0xaa, 0x03, 0x08, 0x08, 0x04, 0x12, 0x04, 0x00, 0x00, 0x00, 0x00]), + } + }, + clientSeq: 0, + noNeedCompatMsg: false + } + }) + return this.packOidbPacket(0x126D, 100, req, true, false); + } + + async packUploadGroupFileReq(groupUin: number, file: PacketMsgFileElement): Promise { + const body = new NapProtoMsg(OidbSvcTrpcTcp0x6D6).encode({ + file: { + groupUin: groupUin, + appId: 4, + busId: 102, + entrance: 6, + targetDirectory: '/', // TODO: + fileName: file.fileName, + localDirectory: `/${file.fileName}`, + fileSize: BigInt(file.fileSize), + fileMd5: file.fileMd5, + fileSha1: file.fileSha1, + fileSha3: Buffer.alloc(0), + field15: true + } + }); + return this.packOidbPacket(0x6D6, 0, body, true, false); + } + + async packUploadC2CFileReq(selfUid: string, peerUid: string, file: PacketMsgFileElement): Promise { + const body = new NapProtoMsg(OidbSvcTrpcTcp0XE37_1700).encode({ + command: 1700, + seq: 0, + upload: { + senderUid: selfUid, + receiverUid: peerUid, + fileSize: file.fileSize, + fileName: file.fileName, + md510MCheckSum: await computeMd5AndLengthWithLimit(file.filePath, 10 * 1024 * 1024), + sha1CheckSum: file.fileSha1, + localPath: "/", + md5CheckSum: file.fileMd5, + sha3CheckSum: Buffer.alloc(0) + }, + businessId: 3, + clientType: 1, + flagSupportMediaPlatform: 1 + }) + return this.packOidbPacket(0xE37, 1700, body, false, false); + } + + packOfflineFileDownloadReq(fileUUID: string, fileHash: string, senderUid: string, receiverUid: string): OidbPacket { + return this.packOidbPacket(0xE37, 800, new NapProtoMsg(OidbSvcTrpcTcp0XE37_800).encode({ + subCommand: 800, + field2: 0, + body: { + senderUid: senderUid, + receiverUid: receiverUid, + fileUuid: fileUUID, + fileHash: fileHash, + }, + field101: 3, + field102: 1, + field200: 1, + }), false, false); + } + + packGroupFileDownloadReq(groupUin: number, fileUUID: string): OidbPacket { + return this.packOidbPacket(0x6D6, 2, new NapProtoMsg(OidbSvcTrpcTcp0x6D6).encode({ download: { groupUin: groupUin, appId: 7, busId: 102, fileId: fileUUID } - }), true, false) + }), true, false ); } packC2CFileDownloadReq(selfUid: string, fileUUID: string, fileHash: string): PacketHexStr { - return this.toHexStr( + return this.packetPacket( new NapProtoMsg(OidbSvcTrpcTcp0XE37_1200).encode({ subCommand: 1200, field2: 1, @@ -321,4 +693,16 @@ export class PacketPacker { }) ); } + + packGroupSignReq(uin: string, groupCode: string): OidbPacket { + return this.packOidbPacket(0XEB7, 1, new NapProtoMsg(OidbSvcTrpcTcp0XEB7).encode( + { + body: { + uin: uin, + groupUin: groupCode, + version: "9.0.90" + } + } + ), false, false); + } } diff --git a/src/core/packet/proto/message/component.ts b/src/core/packet/proto/message/component.ts index 984b5f3a..c83ed1e9 100644 --- a/src/core/packet/proto/message/component.ts +++ b/src/core/packet/proto/message/component.ts @@ -113,8 +113,26 @@ export const Permission = { export const FileExtra = { file: ProtoField(1, () => NotOnlineFile), + field6: ProtoField(6, () => PrivateFileExtra), }; +export const PrivateFileExtra = { + field2: ProtoField(2, () => PrivateFileExtraField2), +} + +export const PrivateFileExtraField2 = { + field1: ProtoField(1, ScalarType.UINT32), + fileUuid: ProtoField(4, ScalarType.STRING), + fileName: ProtoField(5, ScalarType.STRING), + field6: ProtoField(6, ScalarType.UINT32), + field7: ProtoField(7, ScalarType.BYTES), + field8: ProtoField(8, ScalarType.BYTES), + timestamp1: ProtoField(9, ScalarType.UINT32), + fileHash: ProtoField(14, ScalarType.STRING), + selfUid: ProtoField(15, ScalarType.STRING), + destUid: ProtoField(16, ScalarType.STRING), +} + export const GroupFileExtra = { field1: ProtoField(1, ScalarType.UINT32), fileName: ProtoField(2, ScalarType.STRING), @@ -132,8 +150,9 @@ export const GroupFileExtraInfo = { fileSize: ProtoField(3, ScalarType.UINT64), fileName: ProtoField(4, ScalarType.STRING), field5: ProtoField(5, ScalarType.UINT32), - field7: ProtoField(7, ScalarType.STRING), - fileMd5: ProtoField(8, ScalarType.STRING), + fileSha: ProtoField(6, ScalarType.BYTES), + extInfoString: ProtoField(7, ScalarType.STRING), + fileMd5: ProtoField(8, ScalarType.BYTES), }; export const ImageExtraUrl = { diff --git a/src/core/packet/proto/oidb/Oidb.0XE37_800.ts b/src/core/packet/proto/oidb/Oidb.0XE37_800.ts new file mode 100644 index 00000000..f2e6fe5b --- /dev/null +++ b/src/core/packet/proto/oidb/Oidb.0XE37_800.ts @@ -0,0 +1,62 @@ +import { ScalarType } from "@protobuf-ts/runtime"; +import { ProtoField } from "../NapProto"; +import {OidbSvcTrpcTcp0XE37_800_1200Metadata} from "@/core/packet/proto/oidb/Oidb.0xE37_1200"; + +export const OidbSvcTrpcTcp0XE37_800 = { + subCommand: ProtoField(1, ScalarType.UINT32), + field2: ProtoField(2, ScalarType.INT32), + body: ProtoField(10, () => OidbSvcTrpcTcp0XE37_800Body, true), + field101: ProtoField(101, ScalarType.INT32), + field102: ProtoField(102, ScalarType.INT32), + field200: ProtoField(200, ScalarType.INT32) +}; + +export const OidbSvcTrpcTcp0XE37_800Body = { + senderUid: ProtoField(10, ScalarType.STRING, true), + receiverUid: ProtoField(20, ScalarType.STRING, true), + fileUuid: ProtoField(30, ScalarType.STRING, true), + fileHash: ProtoField(40, ScalarType.STRING, true) +}; + +export const OidbSvcTrpcTcp0XE37Response = { + command: ProtoField(1, ScalarType.UINT32), + seq: ProtoField(2, ScalarType.INT32), + upload: ProtoField(19, () => ApplyUploadRespV3, true), + businessId: ProtoField(101, ScalarType.INT32), + clientType: ProtoField(102, ScalarType.INT32), + flagSupportMediaPlatform: ProtoField(200, ScalarType.INT32) +}; + +export const ApplyUploadRespV3 = { + retCode: ProtoField(10, ScalarType.INT32), + retMsg: ProtoField(20, ScalarType.STRING, true), + totalSpace: ProtoField(30, ScalarType.INT64), + usedSpace: ProtoField(40, ScalarType.INT64), + uploadedSize: ProtoField(50, ScalarType.INT64), + uploadIp: ProtoField(60, ScalarType.STRING, true), + uploadDomain: ProtoField(70, ScalarType.STRING, true), + uploadPort: ProtoField(80, ScalarType.UINT32), + uuid: ProtoField(90, ScalarType.STRING, true), + uploadKey: ProtoField(100, ScalarType.BYTES, true), + boolFileExist: ProtoField(110, ScalarType.BOOL), + packSize: ProtoField(120, ScalarType.INT32), + uploadIpList: ProtoField(130, ScalarType.STRING, false, true), // repeated + uploadHttpsPort: ProtoField(140, ScalarType.INT32), + uploadHttpsDomain: ProtoField(150, ScalarType.STRING, true), + uploadDns: ProtoField(160, ScalarType.STRING, true), + uploadLanip: ProtoField(170, ScalarType.STRING, true), + fileAddon: ProtoField(200, ScalarType.STRING, true), + mediaPlatformUploadKey: ProtoField(220, ScalarType.BYTES, true) +}; + +export const OidbSvcTrpcTcp0XE37_800Response = { + command: ProtoField(1, ScalarType.UINT32, true), + subCommand: ProtoField(2, ScalarType.UINT32, true), + body: ProtoField(10, () => OidbSvcTrpcTcp0XE37_800ResponseBody, true), + field50: ProtoField(50, ScalarType.UINT32, true), +}; + +export const OidbSvcTrpcTcp0XE37_800ResponseBody = { + field10: ProtoField(10, ScalarType.UINT32, true), + field30: ProtoField(30, () => OidbSvcTrpcTcp0XE37_800_1200Metadata, true), +} diff --git a/src/core/packet/proto/oidb/Oidb.0xE37_1200.ts b/src/core/packet/proto/oidb/Oidb.0xE37_1200.ts index 80ecbe2c..ba2b0f16 100644 --- a/src/core/packet/proto/oidb/Oidb.0xE37_1200.ts +++ b/src/core/packet/proto/oidb/Oidb.0xE37_1200.ts @@ -30,7 +30,7 @@ export const OidbSvcTrpcTcp0XE37_1200ResponseBody = { field10: ProtoField(10, ScalarType.UINT32, true), state: ProtoField(20, ScalarType.STRING, true), result: ProtoField(30, () => OidbSvcTrpcTcp0XE37_1200Result, true), - metadata: ProtoField(40, () => OidbSvcTrpcTcp0XE37_1200Metadata, true), + metadata: ProtoField(40, () => OidbSvcTrpcTcp0XE37_800_1200Metadata, true), }; export const OidbSvcTrpcTcp0XE37_1200Result = { @@ -43,7 +43,7 @@ export const OidbSvcTrpcTcp0XE37_1200Result = { extra: ProtoField(120, ScalarType.BYTES, true), }; -export const OidbSvcTrpcTcp0XE37_1200Metadata = { +export const OidbSvcTrpcTcp0XE37_800_1200Metadata = { uin: ProtoField(1, ScalarType.UINT32, true), field2: ProtoField(2, ScalarType.UINT32, true), field3: ProtoField(3, ScalarType.UINT32, true), diff --git a/src/core/packet/proto/oidb/Oidb.0xE37_1700.ts b/src/core/packet/proto/oidb/Oidb.0xE37_1700.ts new file mode 100644 index 00000000..8d5c3f73 --- /dev/null +++ b/src/core/packet/proto/oidb/Oidb.0xE37_1700.ts @@ -0,0 +1,23 @@ +import { ScalarType } from "@protobuf-ts/runtime"; +import { ProtoField } from "../NapProto"; + +export const OidbSvcTrpcTcp0XE37_1700 = { + command: ProtoField(1, ScalarType.UINT32, true), + seq: ProtoField(2, ScalarType.INT32, true), + upload: ProtoField(19, () => ApplyUploadReqV3, true), + businessId: ProtoField(101, ScalarType.INT32, true), + clientType: ProtoField(102, ScalarType.INT32, true), + flagSupportMediaPlatform: ProtoField(200, ScalarType.INT32, true), +} + +export const ApplyUploadReqV3 = { + senderUid: ProtoField(10, ScalarType.STRING, true), + receiverUid: ProtoField(20, ScalarType.STRING, true), + fileSize: ProtoField(30, ScalarType.UINT32, true), + fileName: ProtoField(40, ScalarType.STRING, true), + md510MCheckSum: ProtoField(50, ScalarType.BYTES, true), + sha1CheckSum: ProtoField(60, ScalarType.BYTES, true), + localPath: ProtoField(70, ScalarType.STRING, true), + md5CheckSum: ProtoField(110, ScalarType.BYTES, true), + sha3CheckSum: ProtoField(120, ScalarType.BYTES, true), +} diff --git a/src/core/packet/proto/oidb/Oidb.0xEB7.ts b/src/core/packet/proto/oidb/Oidb.0xEB7.ts new file mode 100644 index 00000000..b5543182 --- /dev/null +++ b/src/core/packet/proto/oidb/Oidb.0xEB7.ts @@ -0,0 +1,12 @@ +import { ScalarType } from "@protobuf-ts/runtime"; +import { ProtoField } from "../NapProto"; + +export const OidbSvcTrpcTcp0XEB7_Body = { + uin: ProtoField(1, ScalarType.STRING), + groupUin: ProtoField(2, ScalarType.STRING), + version: ProtoField(3, ScalarType.STRING), +}; + +export const OidbSvcTrpcTcp0XEB7 = { + body: ProtoField(2, () => OidbSvcTrpcTcp0XEB7_Body), +} \ No newline at end of file diff --git a/src/core/packet/utils/crypto/hash.ts b/src/core/packet/utils/crypto/hash.ts index 53901e6c..9576e2c0 100644 --- a/src/core/packet/utils/crypto/hash.ts +++ b/src/core/packet/utils/crypto/hash.ts @@ -2,6 +2,7 @@ import * as crypto from 'crypto'; import * as stream from 'stream'; import * as fs from 'fs'; +import {CalculateStreamBytesTransform} from "@/core/packet/utils/crypto/sha1StreamBytesTransform"; function sha1Stream(readable: stream.Readable) { return new Promise((resolve, reject) => { @@ -10,7 +11,37 @@ function sha1Stream(readable: stream.Readable) { }) as Promise; } +function md5Stream(readable: stream.Readable) { + return new Promise((resolve, reject) => { + readable.on('error', reject); + readable.pipe(crypto.createHash('md5').on('error', reject).on('data', resolve)); + }) as Promise; +} + export function calculateSha1(filePath: string): Promise { const readable = fs.createReadStream(filePath); return sha1Stream(readable); } + +export function computeMd5AndLengthWithLimit(filePath: string, limit?: number): Promise { + const readStream = fs.createReadStream(filePath, limit ? { start: 0, end: limit - 1 } : {}); + return md5Stream(readStream); +} + +export function calculateSha1StreamBytes(filePath: string): Promise { + return new Promise((resolve, reject) => { + const readable = fs.createReadStream(filePath); + const calculateStreamBytes = new CalculateStreamBytesTransform(); + const byteArrayList: Buffer[] = []; + calculateStreamBytes.on('data', (chunk: Buffer) => { + byteArrayList.push(chunk); + }); + calculateStreamBytes.on('end', () => { + resolve(byteArrayList); + }); + calculateStreamBytes.on('error', (err) => { + reject(err); + }); + readable.pipe(calculateStreamBytes); + }); +} diff --git a/src/core/packet/utils/crypto/sha1Stream.test.ts b/src/core/packet/utils/crypto/sha1Stream.test.ts new file mode 100644 index 00000000..b8c9ef7f --- /dev/null +++ b/src/core/packet/utils/crypto/sha1Stream.test.ts @@ -0,0 +1,19 @@ +import crypto from 'crypto'; +import assert from 'assert'; +import { Sha1Stream } from './sha1Stream'; + +function testSha1Stream() { + for (let i = 0; i < 100000; i++) { + const randomLength = Math.floor(Math.random() * 1024); + const randomData = crypto.randomBytes(randomLength); + const sha1Stream = new Sha1Stream(); + sha1Stream.update(randomData); + const hash = sha1Stream.final(); + const expectedDigest = crypto.createHash('sha1').update(randomData).digest(); + assert.strictEqual(hash.toString('hex'), expectedDigest.toString('hex')); + console.log(`Test ${i + 1}: Passed`); + } + console.log('All tests passed successfully.'); +} + +testSha1Stream(); diff --git a/src/core/packet/utils/crypto/sha1Stream.ts b/src/core/packet/utils/crypto/sha1Stream.ts new file mode 100644 index 00000000..f1c344df --- /dev/null +++ b/src/core/packet/utils/crypto/sha1Stream.ts @@ -0,0 +1,118 @@ +export class Sha1Stream { + readonly Sha1BlockSize = 64; + readonly Sha1DigestSize = 20; + private readonly _padding = Buffer.concat([Buffer.from([0x80]), Buffer.alloc(63)]); + private readonly _state = new Uint32Array(5); + private readonly _count = new Uint32Array(2); + private readonly _buffer = Buffer.allocUnsafe(this.Sha1BlockSize); + private readonly _w = new Uint32Array(80); + + constructor() { + this.reset(); + } + + private reset(): void { + this._state[0] = 0x67452301; + this._state[1] = 0xEFCDAB89; + this._state[2] = 0x98BADCFE; + this._state[3] = 0x10325476; + this._state[4] = 0xC3D2E1F0; + this._count[0] = 0; + this._count[1] = 0; + this._buffer.fill(0); + } + + private rotateLeft(v: number, o: number): number { + return ((v << o) | (v >>> (32 - o))) >>> 0; + } + + private transform(chunk: Buffer, offset: number): void { + const w = this._w; + const view = new DataView(chunk.buffer, chunk.byteOffset + offset, 64); + + for (let i = 0; i < 16; i++) { + w[i] = view.getUint32(i * 4, false); + } + + for (let i = 16; i < 80; i++) { + w[i] = this.rotateLeft(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1) >>> 0; + } + + let a = this._state[0]; + let b = this._state[1]; + let c = this._state[2]; + let d = this._state[3]; + let e = this._state[4]; + + for (let i = 0; i < 80; i++) { + const [f, k] = (i < 20) ? [(b & c) | ((~b) & d), 0x5A827999] : + (i < 40) ? [b ^ c ^ d, 0x6ED9EBA1] : + (i < 60) ? [(b & c) | (b & d) | (c & d), 0x8F1BBCDC] : + [b ^ c ^ d, 0xCA62C1D6]; + const temp = (this.rotateLeft(a, 5) + f + k + e + w[i]) >>> 0; + e = d; + d = c; + c = this.rotateLeft(b, 30) >>> 0; + b = a; + a = temp; + } + + this._state[0] = (this._state[0] + a) >>> 0; + this._state[1] = (this._state[1] + b) >>> 0; + this._state[2] = (this._state[2] + c) >>> 0; + this._state[3] = (this._state[3] + d) >>> 0; + this._state[4] = (this._state[4] + e) >>> 0; + } + + public update(data: Buffer, len?: number): void { + let index = ((this._count[0] >>> 3) & 0x3F) >>> 0; + const dataLen = len ?? data.length; + this._count[0] = (this._count[0] + (dataLen << 3)) >>> 0; + + if (this._count[0] < (dataLen << 3)) this._count[1] = (this._count[1] + 1) >>> 0; + + this._count[1] = (this._count[1] + (dataLen >>> 29)) >>> 0; + + let partLen = (this.Sha1BlockSize - index) >>> 0; + let i = 0; + + if (dataLen >= partLen) { + data.copy(this._buffer, index, 0, partLen); + this.transform(this._buffer, 0); + for (i = partLen; (i + this.Sha1BlockSize) <= dataLen; i = (i + this.Sha1BlockSize) >>> 0) { + this.transform(data, i); + } + index = 0; + } + + data.copy(this._buffer, index, i, dataLen); + } + + public hash(bigEndian: boolean = true): Buffer { + const digest = Buffer.allocUnsafe(this.Sha1DigestSize); + if (bigEndian) { + for (let i = 0; i < 5; i++) digest.writeUInt32BE(this._state[i], i * 4); + } else { + for (let i = 0; i < 5; i++) digest.writeUInt32LE(this._state[i], i * 4); + } + return digest; + } + + public final(): Buffer { + const digest = Buffer.allocUnsafe(this.Sha1DigestSize); + const bits = Buffer.allocUnsafe(8) + bits.writeUInt32BE(this._count[1], 0); + bits.writeUInt32BE(this._count[0], 4); + + let index = ((this._count[0] >>> 3) & 0x3F) >>> 0; + const padLen = ((index < 56) ? (56 - index) : (120 - index)) >>> 0; + this.update(this._padding, padLen); + this.update(bits); + + for (let i = 0; i < 5; i++) { + digest.writeUInt32BE(this._state[i], i * 4); + } + + return digest; + } +} diff --git a/src/core/packet/utils/crypto/sha1StreamBytesTransform.ts b/src/core/packet/utils/crypto/sha1StreamBytesTransform.ts new file mode 100644 index 00000000..38fd66eb --- /dev/null +++ b/src/core/packet/utils/crypto/sha1StreamBytesTransform.ts @@ -0,0 +1,53 @@ +import * as stream from "node:stream"; +import {Sha1Stream} from "@/core/packet/utils/crypto/sha1Stream"; + +export class CalculateStreamBytesTransform extends stream.Transform { + private readonly blockSize = 1024 * 1024; + private sha1: Sha1Stream; + private buffer: Buffer; + private bytesRead: number; + private readonly byteArrayList: Buffer[]; + + constructor() { + super(); + this.sha1 = new Sha1Stream(); + this.buffer = Buffer.alloc(0); + this.bytesRead = 0; + this.byteArrayList = []; + } + + _transform(chunk: Buffer, _: BufferEncoding, callback: stream.TransformCallback): void { + try { + this.buffer = Buffer.concat([this.buffer, chunk]); + let offset = 0; + while (this.buffer.length - offset >= this.sha1.Sha1BlockSize) { + const block = this.buffer.subarray(offset, offset + this.sha1.Sha1BlockSize); + this.sha1.update(block); + offset += this.sha1.Sha1BlockSize; + this.bytesRead += this.sha1.Sha1BlockSize; + if (this.bytesRead % this.blockSize === 0) { + const digest = this.sha1.hash(false); + this.byteArrayList.push(Buffer.from(digest)); + } + } + this.buffer = this.buffer.subarray(offset); + callback(null); + } catch (err) { + callback(err as Error); + } + } + + _flush(callback: stream.TransformCallback): void { + try { + if (this.buffer.length > 0) this.sha1.update(this.buffer); + const finalDigest = this.sha1.final(); + this.byteArrayList.push(Buffer.from(finalDigest)); + for (const digest of this.byteArrayList) { + this.push(digest); + } + callback(null); + } catch (err) { + callback(err as Error); + } + } +} diff --git a/src/onebot/action/extends/SetGroupSign.ts b/src/onebot/action/extends/SetGroupSign.ts new file mode 100644 index 00000000..9e87446b --- /dev/null +++ b/src/onebot/action/extends/SetGroupSign.ts @@ -0,0 +1,22 @@ +import BaseAction from '../BaseAction'; +import { ActionName } from '../types'; +import { FromSchema, JSONSchema } from 'json-schema-to-ts'; + +const SchemaData = { + type: 'object', + properties: { + group_id: { type: 'string' }, + }, + required: ['group_id'], +} as const satisfies JSONSchema; + +type Payload = FromSchema; + +export class SetGroupSign extends BaseAction { + actionName = ActionName.SetGroupSign; + payloadSchema = SchemaData; + + async _handle(payload: Payload) { + return await this.core.apis.PacketApi.sendGroupSignPacket(payload.group_id); + } +} diff --git a/src/onebot/action/index.ts b/src/onebot/action/index.ts index 331de2c3..a2cbf7ef 100644 --- a/src/onebot/action/index.ts +++ b/src/onebot/action/index.ts @@ -93,6 +93,7 @@ import { GetGroupFileUrl } from "@/onebot/action/file/GetGroupFileUrl"; import { GetPacketStatus } from "@/onebot/action/packet/GetPacketStatus"; import { FriendPoke } from "@/onebot/action/user/FriendPoke"; import { GetCredentials } from './system/GetCredentials'; +import { SetGroupSign } from './extends/SetGroupSign'; export type ActionMap = Map>; @@ -115,6 +116,7 @@ export function createActionMap(obContext: NapCatOneBot11Adapter, core: NapCatCo new SetQQAvatar(obContext, core), new TranslateEnWordToZn(obContext, core), new GetGroupRootFiles(obContext, core), + new SetGroupSign(obContext, core), // onebot11 new SendLike(obContext, core), new GetMsg(obContext, core), diff --git a/src/onebot/action/msg/SendMsg.ts b/src/onebot/action/msg/SendMsg.ts index eab2b2ae..f0536190 100644 --- a/src/onebot/action/msg/SendMsg.ts +++ b/src/onebot/action/msg/SendMsg.ts @@ -117,7 +117,7 @@ export class SendMsg extends BaseAction { if (getSpecialMsgNum(payload, OB11MessageDataType.node)) { const packetMode = this.core.apis.PacketApi.available; const returnMsgAndResId = packetMode - ? await this.handleForwardedNodesPacket(peer, messages as OB11MessageNode[]) + ? await this.handleForwardedNodesPacket(peer, messages as OB11MessageNode[], payload.source, payload.news, payload.summary, payload.prompt) : await this.handleForwardedNodes(peer, messages as OB11MessageNode[]); if (returnMsgAndResId.message) { const msgShortId = MessageUnique.createUniqueMsgId({ @@ -146,7 +146,7 @@ export class SendMsg extends BaseAction { } // TODO: recursively handle forwarded nodes - private async handleForwardedNodesPacket(msgPeer: Peer, messageNodes: OB11MessageNode[]): Promise<{ + private async handleForwardedNodesPacket(msgPeer: Peer, messageNodes: OB11MessageNode[], source?: string, news?: { text: string }[], summary?: string, prompt?: string): Promise<{ message: RawMessage | null, res_id?: string }> { @@ -157,7 +157,7 @@ export class SendMsg extends BaseAction { const OB11Data = normalize(node.data.content); const { sendElements } = await this.obContext.apis.MsgApi.createSendElements(OB11Data, msgPeer); const packetMsgElements: rawMsgWithSendMsg = { - senderUin: node.data.user_id ?? +this.core.selfInfo.uin, + senderUin: Number(node.data.user_id) ?? +this.core.selfInfo.uin, senderName: node.data.nickname, groupId: msgPeer.chatType === ChatType.KCHATTYPEGROUP ? +msgPeer.peerUid : undefined, time: Date.now(), @@ -172,7 +172,7 @@ export class SendMsg extends BaseAction { } } const resid = await this.core.apis.PacketApi.sendUploadForwardMsg(packetMsg, msgPeer.chatType === ChatType.KCHATTYPEGROUP ? +msgPeer.peerUid : 0); - const forwardJson = ForwardMsgBuilder.fromPacketMsg(resid, packetMsg); + const forwardJson = ForwardMsgBuilder.fromPacketMsg(resid, packetMsg, source, news, summary, prompt); const finallySendElements = { elementType: ElementType.ARK, elementId: "", diff --git a/src/onebot/action/types.ts b/src/onebot/action/types.ts index 509593b4..7ab3f71a 100644 --- a/src/onebot/action/types.ts +++ b/src/onebot/action/types.ts @@ -134,5 +134,7 @@ export enum ActionName { GetGuildProfile = 'get_guild_service_profile', GetGroupIgnoredNotifies = 'get_group_ignored_notifies', + + SetGroupSign = "set_group_sign", // UploadForwardMsg = "upload_forward_msg", } diff --git a/src/onebot/types/message.ts b/src/onebot/types/message.ts index d4890e34..37830023 100644 --- a/src/onebot/types/message.ts +++ b/src/onebot/types/message.ts @@ -16,8 +16,8 @@ export interface OB11Message { message_id: number, message_seq: number, // 和message_id一样 real_id: number, - user_id: number, - group_id?: number, + user_id: number | string, // number + group_id?: number | string, // number message_type: 'private' | 'group', sub_type?: 'friend' | 'group' | 'normal', sender: OB11Sender, @@ -149,7 +149,7 @@ export interface OB11MessageNode { type: OB11MessageDataType.node; data: { id?: string - user_id?: number + user_id?: number | string // number nickname: string content: OB11MessageMixType }; @@ -221,6 +221,10 @@ export interface OB11PostSendMsg { message: OB11MessageMixType; messages?: OB11MessageMixType; // 兼容 go-cqhttp auto_escape?: boolean | string + source?: string, + news?: { text: string }[], + summary?: string, + prompt?: string } export interface OB11PostContext { message_type?: 'private' | 'group' diff --git a/src/webui/ui/NapCat.ts b/src/webui/ui/NapCat.ts index 55ccf7c0..f1e072db 100644 --- a/src/webui/ui/NapCat.ts +++ b/src/webui/ui/NapCat.ts @@ -30,7 +30,7 @@ async function onSettingWindowCreated(view: Element) { SettingItem( 'Napcat', undefined, - SettingButton('V3.1.3', 'napcat-update-button', 'secondary'), + SettingButton('V3.1.5', 'napcat-update-button', 'secondary'), ), ]), SettingList([ diff --git a/static/assets/renderer.js b/static/assets/renderer.js index 3ee3d4f7..90ea8978 100644 --- a/static/assets/renderer.js +++ b/static/assets/renderer.js @@ -164,7 +164,7 @@ async function onSettingWindowCreated(view) { SettingItem( 'Napcat', void 0, - SettingButton("V3.1.3", "napcat-update-button", "secondary") + SettingButton("V3.1.5", "napcat-update-button", "secondary") ) ]), SettingList([