diff --git a/README.md b/README.md
index 081bd51e..a00b37b8 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,7 @@
-

+
+ 
+
---
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([