Merge branch 'main' into laana

# Conflicts:
#	package.json
This commit is contained in:
Wesley F. Young 2024-10-25 09:22:23 +08:00
commit 491db5b209
31 changed files with 1625 additions and 212 deletions

View File

@ -1,5 +1,7 @@
<div align="center"> <div align="center">
<img src="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" alt="NapCatQQ" width="640" height="320" />
![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)
</div> </div>
--- ---

View File

@ -4,7 +4,7 @@
"name": "NapCatQQ", "name": "NapCatQQ",
"slug": "NapCat.Framework", "slug": "NapCat.Framework",
"description": "高性能的 OneBot 11 协议实现", "description": "高性能的 OneBot 11 协议实现",
"version": "3.1.3", "version": "3.1.5",
"icon": "./logo.png", "icon": "./logo.png",
"authors": [ "authors": [
{ {

View File

@ -2,7 +2,7 @@
"name": "napcat", "name": "napcat",
"private": true, "private": true,
"type": "module", "type": "module",
"version": "3.1.3", "version": "3.1.5",
"scripts": { "scripts": {
"build:framework": "vite build --mode framework", "build:framework": "vite build --mode framework",
"build:shell": "vite build --mode shell", "build:shell": "vite build --mode shell",
@ -21,7 +21,6 @@
"@types/express": "^5.0.0", "@types/express": "^5.0.0",
"@types/fluent-ffmpeg": "^2.1.24", "@types/fluent-ffmpeg": "^2.1.24",
"@types/fs-extra": "^11.0.4", "@types/fs-extra": "^11.0.4",
"@types/jest": "^29.5.12",
"@types/node": "^22.0.1", "@types/node": "^22.0.1",
"@types/qrcode-terminal": "^0.12.2", "@types/qrcode-terminal": "^0.12.2",
"@types/ws": "^8.5.12", "@types/ws": "^8.5.12",
@ -37,9 +36,8 @@
"eslint-plugin-import": "^2.29.1", "eslint-plugin-import": "^2.29.1",
"fast-xml-parser": "^4.3.6", "fast-xml-parser": "^4.3.6",
"file-type": "^19.0.0", "file-type": "^19.0.0",
"fluent-ffmpeg": "^2.1.2",
"image-size": "^1.1.1", "image-size": "^1.1.1",
"json-schema-to-ts": "^3.1.0", "json-schema-to-ts": "^3.1.1",
"typescript": "^5.3.3", "typescript": "^5.3.3",
"vite": "5.4.6", "vite": "5.4.6",
"vite-plugin-cp": "^4.0.8", "vite-plugin-cp": "^4.0.8",

View File

@ -50,9 +50,29 @@ interface ForwardAdaptMsgElement {
} }
export class ForwardMsgBuilder { 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 id = crypto.randomUUID();
const isGroupMsg = msg.some(m => m.isGroupMsg); 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 { return {
app: "com.tencent.multimsg", app: "com.tencent.multimsg",
config: { config: {
@ -62,29 +82,21 @@ export class ForwardMsgBuilder {
type: "normal", type: "normal",
width: 300 width: 300
}, },
desc: "[聊天记录]", desc: prompt,
extra: { extra: {
filename: id, filename: id,
tsum: msg.length, tsum: msg.length,
}, },
meta: { meta: {
detail: { detail: {
news: msg.length === 0 ? [{ news,
text: "Nya~ This message is send from NapCat.Packet!",
}] : msg.map(m => ({
text: `${m.senderName}: ${m.msg?.map(msg => msg.preview).join('')}`,
})),
resid: resId, resid: resId,
source: isGroupMsg ? "群聊的聊天记录" : source,
msg.length summary,
? Array.from(new Set(msg.map(m => m.senderName)))
.join('和') + '的聊天记录'
: '聊天记录',
summary: `查看${msg.length}条转发消息`,
uniseq: id, uniseq: id,
} }
}, },
prompt: "[聊天记录]", prompt,
ver: "0.0.0.5", ver: "0.0.0.5",
view: "contact", view: "contact",
}; };
@ -94,13 +106,13 @@ export class ForwardMsgBuilder {
return this.build(resId, []); 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 => ({ return this.build(resId, packetMsg.map(msg => ({
senderName: msg.senderName, senderName: msg.senderName,
isGroupMsg: msg.groupId !== undefined, isGroupMsg: msg.groupId !== undefined,
msg: msg.msg.map(m => ({ msg: msg.msg.map(m => ({
preview: m.toPreview(), preview: m.valid? m.toPreview() : "[该消息类型暂不支持查看]",
})) }))
}))); })), source, news, summary, prompt);
} }
} }

View File

@ -1 +1 @@
export const napCatVersion = '3.1.3'; export const napCatVersion = '3.1.5';

View File

@ -3,7 +3,7 @@ import { ChatType, InstanceContext, NapCatCore } from '..';
import offset from '@/core/external/offset.json'; import offset from '@/core/external/offset.json';
import { PacketClient, RecvPacketData } from '@/core/packet/client'; import { PacketClient, RecvPacketData } from '@/core/packet/client';
import { PacketSession } from "@/core/packet/session"; 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 { NapProtoMsg } from '@/core/packet/proto/NapProto';
import { OidbSvcTrpcTcp0X9067_202_Rsp_Body } from '@/core/packet/proto/oidb/Oidb.0x9067_202'; import { OidbSvcTrpcTcp0X9067_202_Rsp_Body } from '@/core/packet/proto/oidb/Oidb.0x9067_202';
import { OidbSvcTrpcTcpBase, OidbSvcTrpcTcpBaseRsp } from '@/core/packet/proto/oidb/OidbBase'; 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 { SendLongMsgResp } from "@/core/packet/proto/message/action";
import { PacketMsg } from "@/core/packet/msg/message"; import { PacketMsg } from "@/core/packet/msg/message";
import { OidbSvcTrpcTcp0x6D6Response } from "@/core/packet/proto/oidb/Oidb.0x6D6"; 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 { interface OffsetType {
@ -73,25 +78,32 @@ export class NTQQPacketApi {
return this.packetSession!.client.sendPacket(cmd, data, rsp); return this.packetSession!.client.sendPacket(cmd, data, rsp);
} }
async sendOidbPacket(pkt: OidbPacket, rsp = false): Promise<RecvPacketData> {
return this.sendPacket(pkt.cmd, pkt.data, rsp);
}
async sendPokePacket(peer: number, group?: number) { async sendPokePacket(peer: number, group?: number) {
const data = this.packetSession?.packer.packPokePacket(peer, group); const data = this.packetSession?.packer.packPokePacket(peer, group);
await this.sendPacket('OidbSvcTrpcTcp.0xed3_1', data!, false); await this.sendOidbPacket(data!, false);
} }
async sendRkeyPacket() { async sendRkeyPacket() {
const packet = this.packetSession?.packer.packRkeyPacket(); 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 []; if (!ret?.hex_data) return [];
const body = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(Buffer.from(ret.hex_data, 'hex')).body; const body = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(Buffer.from(ret.hex_data, 'hex')).body;
const retData = new NapProtoMsg(OidbSvcTrpcTcp0X9067_202_Rsp_Body).decode(body); const retData = new NapProtoMsg(OidbSvcTrpcTcp0X9067_202_Rsp_Body).decode(body);
return retData.data.rkeyList; 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> { async sendStatusPacket(uin: number): Promise<{ status: number; ext_status: number; } | undefined> {
let status = 0; let status = 0;
try { try {
const packet = this.packetSession?.packer.packStatusPacket(uin); 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 data = Buffer.from(ret.hex_data, 'hex');
const ext = new NapProtoMsg(OidbSvcTrpcTcp0XFE1_2RSP).decode(new NapProtoMsg(OidbSvcTrpcTcpBase).decode(data).body).data.status.value; const ext = new NapProtoMsg(OidbSvcTrpcTcp0XFE1_2RSP).decode(new NapProtoMsg(OidbSvcTrpcTcpBase).decode(data).body).data.status.value;
// ext & 0xff00 + ext >> 16 & 0xff // ext & 0xff00 + ext >> 16 & 0xff
@ -108,22 +120,47 @@ export class NTQQPacketApi {
async sendSetSpecialTittlePacket(groupCode: string, uid: string, tittle: string) { async sendSetSpecialTittlePacket(groupCode: string, uid: string, tittle: string) {
const data = this.packetSession?.packer.packSetSpecialTittlePacket(groupCode, uid, tittle); 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 = []; const reqList = [];
for (const m of msg) { for (const m of msg) {
for (const e of m.msg) { for (const e of m.msg) {
if (e instanceof PacketMsgPicElement) { if (e instanceof PacketMsgPicElement) {
reqList.push(this.packetSession?.highwaySession.uploadImage({ reqList.push(this.packetSession?.highwaySession.uploadImage({
chatType: groupUin ? ChatType.KCHATTYPEGROUP : ChatType.KCHATTYPEC2C, 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)); }, 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) { async sendUploadForwardMsg(msg: PacketMsg[], groupUin: number = 0) {
@ -137,7 +174,7 @@ export class NTQQPacketApi {
async sendGroupFileDownloadReq(groupUin: number, fileUUID: string) { async sendGroupFileDownloadReq(groupUin: number, fileUUID: string) {
const data = this.packetSession?.packer.packGroupFileDownloadReq(groupUin, fileUUID); 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 body = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(Buffer.from(ret.hex_data, 'hex')).body;
const resp = new NapProtoMsg(OidbSvcTrpcTcp0x6D6Response).decode(body); const resp = new NapProtoMsg(OidbSvcTrpcTcp0x6D6Response).decode(body);
if (resp.download.retCode !== 0) { if (resp.download.retCode !== 0) {

View File

@ -50,5 +50,13 @@
"9.9.16-28788": { "9.9.16-28788": {
"appid": 537249739, "appid": 537249739,
"qua": "V1_WIN_NQ_9.9.16_28788_GW_B" "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"
} }
} }

View File

@ -19,8 +19,20 @@
"send": "A0CEC20", "send": "A0CEC20",
"recv": "A0D2520" "recv": "A0D2520"
}, },
"3.2.13-28788-arm64":{ "3.2.13-28788-arm64": {
"send": "6E91018", "send": "6E91018",
"recv": "6E94850" "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"
} }
} }

View File

@ -3,8 +3,7 @@ import { LRUCache } from "@/common/lru-cache";
import WebSocket, { Data } from "ws"; import WebSocket, { Data } from "ws";
import crypto, { createHash } from "crypto"; import crypto, { createHash } from "crypto";
import { NapCatCore } from "@/core"; import { NapCatCore } from "@/core";
import { PacketHexStr } from "@/core/packet/packer"; import { OidbPacket, PacketHexStr } from "@/core/packet/packer";
import { sleep } from "@/common/helper";
export interface RecvPacket { export interface RecvPacket {
type: string, // 仅recv type: string, // 仅recv
@ -177,4 +176,8 @@ export class PacketClient {
}).then((res) => resolve(res)).catch((e: Error) => reject(e)); }).then((res) => resolve(res)).catch((e: Error) => reject(e));
}); });
} }
async sendOidbPacket(pkt: OidbPacket, rsp = false): Promise<RecvPacketData> {
return this.sendPacket(pkt.cmd, pkt.data, rsp);
}
} }

View File

@ -36,7 +36,7 @@ export class PacketHighwayClient {
this.port = port; 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 { return {
uin: this.sig.uin, uin: this.sig.uin,
cmd: cmd, cmd: cmd,

View File

@ -8,9 +8,17 @@ import { HttpConn0x6ff_501Response } from "@/core/packet/proto/action/action";
import { PacketHighwayClient } from "@/core/packet/highway/client"; import { PacketHighwayClient } from "@/core/packet/highway/client";
import { NTV2RichMediaResp } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaResp"; import { NTV2RichMediaResp } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaResp";
import { OidbSvcTrpcTcpBaseRsp } from "@/core/packet/proto/oidb/OidbBase"; import { OidbSvcTrpcTcpBaseRsp } from "@/core/packet/proto/oidb/OidbBase";
import { PacketMsgPicElement } from "@/core/packet/msg/element"; import {
import { NTV2RichMediaHighwayExt } from "@/core/packet/proto/highway/highway"; 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 { 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; export const BlockSize = 1024 * 1024;
@ -21,6 +29,7 @@ interface HighwayServerAddr {
export interface PacketHighwaySig { export interface PacketHighwaySig {
uin: string; uin: string;
uid: string;
sigSession: Uint8Array | null sigSession: Uint8Array | null
sessionKey: Uint8Array | null sessionKey: Uint8Array | null
serverAddr: HighwayServerAddr[] serverAddr: HighwayServerAddr[]
@ -39,6 +48,7 @@ export class PacketHighwaySession {
this.logger = logger; this.logger = logger;
this.sig = { this.sig = {
uin: this.packetClient.napCatCore.selfInfo.uin, uin: this.packetClient.napCatCore.selfInfo.uin,
uid: this.packetClient.napCatCore.selfInfo.uid,
sigSession: null, sigSession: null,
sessionKey: null, sessionKey: null,
serverAddr: [], serverAddr: [],
@ -49,7 +59,6 @@ export class PacketHighwaySession {
private async checkAvailable() { private async checkAvailable() {
if (!this.packetClient.available) { if (!this.packetClient.available) {
this.logger.logError('[Highway] packetServer not available!');
throw new Error('packetServer不可用请参照文档 https://napneko.github.io/config/advanced 检查packetServer状态或进行配置'); throw new Error('packetServer不可用请参照文档 https://napneko.github.io/config/advanced 检查packetServer状态或进行配置');
} }
if (this.sig.sigSession === null || this.sig.sessionKey === null) { if (this.sig.sigSession === null || this.sig.sessionKey === null) {
@ -86,7 +95,7 @@ export class PacketHighwaySession {
async uploadImage(peer: Peer, img: PacketMsgPicElement): Promise<void> { async uploadImage(peer: Peer, img: PacketMsgPicElement): Promise<void> {
await this.checkAvailable(); await this.checkAvailable();
if (peer.chatType === ChatType.KCHATTYPEGROUP) { if (peer.chatType === ChatType.KCHATTYPEGROUP) {
await this.uploadGroupImageReq(Number(peer.peerUid), img); await this.uploadGroupImageReq(+peer.peerUid, img);
} else if (peer.chatType === ChatType.KCHATTYPEC2C) { } else if (peer.chatType === ChatType.KCHATTYPEC2C) {
await this.uploadC2CImageReq(peer.peerUid, img); await this.uploadC2CImageReq(peer.peerUid, img);
} else { } else {
@ -94,16 +103,53 @@ export class PacketHighwaySession {
} }
} }
async uploadVideo(peer: Peer, video: PacketMsgVideoElement): Promise<void> {
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<void> {
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<void> {
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<void> { private async uploadGroupImageReq(groupUin: number, img: PacketMsgPicElement): Promise<void> {
img.sha1 = Buffer.from(await calculateSha1(img.path)).toString('hex');
const preReq = await this.packer.packUploadGroupImgReq(groupUin, img); 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( const preResp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(
Buffer.from(preRespRaw.hex_data, 'hex') Buffer.from(preRespRaw.hex_data, 'hex')
); );
const preRespData = new NapProtoMsg(NTV2RichMediaResp).decode(preResp.body); const preRespData = new NapProtoMsg(NTV2RichMediaResp).decode(preResp.body);
const ukey = preRespData.upload.uKey; const ukey = preRespData.upload.uKey;
if (ukey && 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 index = preRespData.upload.msgInfo.msgInfoBody[0].index;
const sha1 = Buffer.from(index.info.fileSha1, 'hex'); const sha1 = Buffer.from(index.info.fileSha1, 'hex');
const md5 = Buffer.from(index.info.fileHash, 'hex'); const md5 = Buffer.from(index.info.fileHash, 'hex');
@ -121,28 +167,29 @@ export class PacketHighwaySession {
}); });
await this.packetHighwayClient.upload( await this.packetHighwayClient.upload(
1004, 1004,
fs.createReadStream(img.path, { highWaterMark: BlockSize }), fs.createReadStream(img.path, {highWaterMark: BlockSize}),
img.size, img.size,
md5, md5,
extend extend
); );
} else { } 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.msgInfo = preRespData.upload.msgInfo;
// img.groupPicExt = new NapProtoMsg(CustomFace).decode(preRespData.tcpUpload.compatQMsg) // img.groupPicExt = new NapProtoMsg(CustomFace).decode(preRespData.tcpUpload.compatQMsg)
} }
private async uploadC2CImageReq(peerUid: string, img: PacketMsgPicElement): Promise<void> { private async uploadC2CImageReq(peerUid: string, img: PacketMsgPicElement): Promise<void> {
img.sha1 = Buffer.from(await calculateSha1(img.path)).toString('hex');
const preReq = await this.packer.packUploadC2CImgReq(peerUid, img); 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( const preResp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(
Buffer.from(preRespRaw.hex_data, 'hex') Buffer.from(preRespRaw.hex_data, 'hex')
); );
const preRespData = new NapProtoMsg(NTV2RichMediaResp).decode(preResp.body); const preRespData = new NapProtoMsg(NTV2RichMediaResp).decode(preResp.body);
const ukey = preRespData.upload.uKey; const ukey = preRespData.upload.uKey;
if (ukey && 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 index = preRespData.upload.msgInfo.msgInfoBody[0].index;
const sha1 = Buffer.from(index.info.fileSha1, 'hex'); const sha1 = Buffer.from(index.info.fileSha1, 'hex');
const md5 = Buffer.from(index.info.fileHash, 'hex'); const md5 = Buffer.from(index.info.fileHash, 'hex');
@ -160,12 +207,364 @@ export class PacketHighwaySession {
}); });
await this.packetHighwayClient.upload( await this.packetHighwayClient.upload(
1003, 1003,
fs.createReadStream(img.path, { highWaterMark: BlockSize }), fs.createReadStream(img.path, {highWaterMark: BlockSize}),
img.size, img.size,
md5, md5,
extend extend
); );
} else {
this.logger.logDebug(`[Highway] uploadC2CImageReq get upload invalid ukey ${ukey}, don't need upload!`);
} }
img.msgInfo = preRespData.upload.msgInfo; img.msgInfo = preRespData.upload.msgInfo;
} }
private async uploadGroupVideoReq(groupUin: number, video: PacketMsgVideoElement): Promise<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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;
}
} }

View File

@ -19,11 +19,20 @@ abstract class HighwayUploader {
this.logger = logger; this.logger = logger;
} }
encryptTransExt(key: Uint8Array) { private encryptTransExt(key: Uint8Array) {
if (!this.trans.encrypt) return; if (!this.trans.encrypt) return;
this.trans.ext = tea.encrypt(Buffer.from(this.trans.ext), Buffer.from(key)); this.trans.ext = tea.encrypt(Buffer.from(this.trans.ext), Buffer.from(key));
} }
protected timeout(): Promise<void> {
return new Promise<void>((_, 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 { buildPicUpHead(offset: number, bodyLength: number, bodyMd5: Uint8Array): Uint8Array {
return new NapProtoMsg(ReqDataHighwayHead).encode({ return new NapProtoMsg(ReqDataHighwayHead).encode({
msgBaseHead: { msgBaseHead: {
@ -86,15 +95,18 @@ class HighwayTcpUploaderTransform extends stream.Transform {
export class HighwayTcpUploader extends HighwayUploader { export class HighwayTcpUploader extends HighwayUploader {
async upload(): Promise<void> { async upload(): Promise<void> {
const highwayTransForm = new HighwayTcpUploaderTransform(this); const controller = new AbortController();
const upload = new Promise<void>((resolve, _) => { const { signal } = controller;
const upload = new Promise<void>((resolve, reject) => {
const highwayTransForm = new HighwayTcpUploaderTransform(this);
const socket = net.connect(this.trans.port, this.trans.server, () => { 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 handleRspHeader = (header: Buffer) => {
const rsp = new NapProtoMsg(RespDataHighwayHead).decode(header); const rsp = new NapProtoMsg(RespDataHighwayHead).decode(header);
if (rsp.errorCode !== 0) { 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); 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')}`); 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) => { socket.on('data', (chunk: Buffer) => {
try { if (signal.aborted) {
const [head, _] = Frame.unpack(chunk); socket.end();
handleRspHeader(head); reject(new Error('Upload aborted due to timeout'));
} catch (e) {
this.logger.logError(`[Highway] tcpUpload parse response error: ${e}`);
} }
const [head, _] = Frame.unpack(chunk);
handleRspHeader(head);
}); });
socket.on('close', () => { socket.on('close', () => {
this.logger.logDebug('[Highway] tcpUpload socket closed.'); this.logger.logDebug('[Highway] tcpUpload socket closed.');
resolve(); resolve();
}); });
socket.on('error', (err) => { 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.trans.data.on('error', (err) => {
this.logger.logError('[Highway] tcpUpload readable error:', err);
socket.end(); socket.end();
reject(new Error(`[Highway] tcpUpload readable error: ${err}`));
}); });
}); });
const timeout = new Promise<void>((_, reject) => { const timeout = this.timeout().catch((err) => {
setTimeout(() => { controller.abort();
reject(new Error(`[Highway] tcpUpload timeout after ${this.trans.timeout}s`)); throw new Error(err.message);
}, (this.trans.timeout ?? Infinity) * 1000
);
}); });
await Promise.race([upload, timeout]); await Promise.race([upload, timeout]);
} }
} }
// TODO: timeout impl
export class HighwayHttpUploader extends HighwayUploader { export class HighwayHttpUploader extends HighwayUploader {
async upload(): Promise<void> { async upload(): Promise<void> {
let offset = 0; const controller = new AbortController();
for await (const chunk of this.trans.data) { const { signal } = controller;
const block = chunk as Buffer; const upload = (async () => {
try { let offset = 0;
await this.uploadBlock(block, offset); for await (const chunk of this.trans.data) {
} catch (err) { if (signal.aborted) {
this.logger.logError(`[Highway] httpUpload Error uploading block at offset ${offset}: ${err}`); throw new Error('Upload aborted due to timeout');
throw err; }
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<void> { private async uploadBlock(block: Buffer, offset: number): Promise<void> {
@ -158,9 +179,7 @@ export class HighwayHttpUploader extends HighwayUploader {
const [head, body] = Frame.unpack(resp); const [head, body] = Frame.unpack(resp);
const headData = new NapProtoMsg(RespDataHighwayHead).decode(head); 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')}`); this.logger.logDebug(`[Highway] httpUploadBlock: ${headData.errorCode} | ${headData.msgSegHead?.retCode} | ${headData.bytesRspExtendInfo} | ${head.toString('hex')} | ${body.toString('hex')}`);
if (headData.errorCode !== 0) { if (headData.errorCode !== 0) throw new Error(`[Highway] httpUploadBlock failed (code=${headData.errorCode})`);
this.logger.logError(`[Highway] httpUploadBlock failed (code=${headData.errorCode})`);
}
} }
private async httpPostHighwayContent(frame: Buffer, serverURL: string): Promise<Buffer> { private async httpPostHighwayContent(frame: Buffer, serverURL: string): Promise<Buffer> {
@ -176,12 +195,12 @@ export class HighwayHttpUploader extends HighwayUploader {
}, },
}; };
const req = http.request(serverURL, options, (res) => { const req = http.request(serverURL, options, (res) => {
let data = Buffer.alloc(0); const data: Buffer[] = [];
res.on('data', (chunk) => { res.on('data', (chunk) => {
data = Buffer.concat([data, chunk]); data.push(chunk);
}); });
res.on('end', () => { res.on('end', () => {
resolve(data); resolve(Buffer.concat(data));
}); });
}); });
req.write(frame); req.write(frame);

View File

@ -2,7 +2,9 @@ import * as crypto from "crypto";
import { PushMsgBody } from "@/core/packet/proto/message/message"; import { PushMsgBody } from "@/core/packet/proto/message/message";
import { NapProtoEncodeStructType } from "@/core/packet/proto/NapProto"; import { NapProtoEncodeStructType } from "@/core/packet/proto/NapProto";
import { LogWrapper } from "@/common/log"; 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 { export class PacketMsgBuilder {
private logger: LogWrapper; private logger: LogWrapper;
@ -11,10 +13,23 @@ export class PacketMsgBuilder {
this.logger = logger; this.logger = logger;
} }
protected static failBackText = new PacketMsgTextElement(
{
textElement: {content: "[该消息类型暂不支持查看]"}!
} as SendTextElement
)
buildFakeMsg(selfUid: string, element: PacketMsg[]): NapProtoEncodeStructType<typeof PushMsgBody>[] { buildFakeMsg(selfUid: string, element: PacketMsg[]): NapProtoEncodeStructType<typeof PushMsgBody>[] {
return element.map((node): NapProtoEncodeStructType<typeof PushMsgBody> => { return element.map((node): NapProtoEncodeStructType<typeof PushMsgBody> => {
const avatar = `https://q.qlogo.cn/headimg_dl?dst_uin=${node.senderUin}&spec=640&img_type=jpg`; 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<PacketSendMsgElement>) => {
return acc !== undefined ? acc : msg.buildContent();
}, undefined);
const msgElement = node.msg.flatMap(msg => msg.buildElement() ?? []); 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 { return {
responseHead: { responseHead: {
fromUid: "", fromUid: "",
@ -50,7 +65,8 @@ export class PacketMsgBuilder {
body: { body: {
richText: { richText: {
elems: msgElement elems: msgElement
} },
msgContent: msgContent,
} }
}; };
}); });

View File

@ -1,4 +1,3 @@
import assert from "node:assert";
import * as zlib from "node:zlib"; import * as zlib from "node:zlib";
import { NapProtoEncodeStructType, NapProtoMsg } from "@/core/packet/proto/NapProto"; import { NapProtoEncodeStructType, NapProtoMsg } from "@/core/packet/proto/NapProto";
import { import {
@ -28,6 +27,8 @@ import {
import { MsgInfo } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaReq"; import { MsgInfo } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaReq";
import { PacketMsg, PacketSendMsgElement } from "@/core/packet/msg/message"; import { PacketMsg, PacketSendMsgElement } from "@/core/packet/msg/message";
import { ForwardMsgBuilder } from "@/common/forward-msg-builder"; 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 // raw <-> packet
// TODO: SendStructLongMsgElement // TODO: SendStructLongMsgElement
@ -35,16 +36,20 @@ export abstract class IPacketMsgElement<T extends PacketSendMsgElement> {
protected constructor(rawElement: T) { protected constructor(rawElement: T) {
} }
get valid(): boolean {
return true;
}
buildContent(): Uint8Array | undefined { buildContent(): Uint8Array | undefined {
return undefined; return undefined;
} }
buildElement(): NapProtoEncodeStructType<typeof Elem>[] | undefined { buildElement(): NapProtoEncodeStructType<typeof Elem>[] {
return undefined; return [];
} }
toPreview(): string { toPreview(): string {
return '[nya~]'; return '[暂不支持该消息类型喵~]';
} }
} }
@ -84,59 +89,15 @@ export class PacketMsgAtElement extends PacketMsgTextElement {
text: { text: {
str: this.text, str: this.text,
pbReserve: new NapProtoMsg(MentionExtra).encode({ pbReserve: new NapProtoMsg(MentionExtra).encode({
type: this.atAll ? 1 : 2, type: this.atAll ? 1 : 2,
uin: 0, uin: 0,
field5: 0, field5: 0,
uid: this.targetUid, uid: this.targetUid,
} }
) )
} }
}]; }];
} }
toPreview(): string {
return `@${this.targetUid} ${this.text}`;
}
}
export class PacketMsgPicElement extends IPacketMsgElement<SendPicElement> {
path: string;
name: string;
size: number;
md5: string;
width: number;
height: number;
picType: PicType;
sha1: string | null = null;
msgInfo: NapProtoEncodeStructType<typeof MsgInfo> | null = null;
groupPicExt: NapProtoEncodeStructType<typeof CustomFace> | null = null;
c2cPicExt: NapProtoEncodeStructType<typeof NotOnlineImage> | 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<typeof Elem>[] {
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<SendReplyElement> { export class PacketMsgReplyElement extends IPacketMsgElement<SendReplyElement> {
@ -151,11 +112,11 @@ export class PacketMsgReplyElement extends IPacketMsgElement<SendReplyElement> {
constructor(element: SendReplyElement) { constructor(element: SendReplyElement) {
super(element); super(element);
this.messageId = BigInt(element.replyElement.replayMsgId ?? 0); this.messageId = BigInt(element.replyElement.replayMsgId ?? 0);
this.messageSeq = Number(element.replyElement.replayMsgSeq ?? 0); this.messageSeq = +(element.replyElement.replayMsgSeq ?? 0);
this.messageClientSeq = Number(element.replyElement.replyMsgClientSeq ?? 0); this.messageClientSeq = +(element.replyElement.replyMsgClientSeq ?? 0);
this.targetUin = Number(element.replyElement.senderUin ?? 0); this.targetUin = +(element.replyElement.senderUin ?? 0);
this.targetUid = element.replyElement.senderUidStr ?? ''; this.targetUid = element.replyElement.senderUidStr ?? '';
this.time = Number(element.replyElement.replyMsgTime ?? 0); this.time = +(element.replyElement.replyMsgTime ?? 0);
this.elems = []; // TODO: in replyElement.sourceMsgTextElems this.elems = []; // TODO: in replyElement.sourceMsgTextElems
} }
@ -189,7 +150,7 @@ export class PacketMsgReplyElement extends IPacketMsgElement<SendReplyElement> {
} }
toPreview(): string { toPreview(): string {
return "[回复]"; return "[回复消息]";
} }
} }
@ -284,21 +245,216 @@ export class PacketMsgMarkFaceElement extends IPacketMsgElement<SendMarketFaceEl
} }
} }
export class PacketMsgVideoElement extends IPacketMsgElement<SendVideoElement> { export class PacketMsgPicElement extends IPacketMsgElement<SendPicElement> {
constructor(element: SendVideoElement) { path: string;
name: string;
size: number;
md5: string;
width: number;
height: number;
picType: PicType;
sha1: string | null = null;
msgInfo: NapProtoEncodeStructType<typeof MsgInfo> | null = null;
groupPicExt: NapProtoEncodeStructType<typeof CustomFace> | null = null;
c2cPicExt: NapProtoEncodeStructType<typeof NotOnlineImage> | null = null;
constructor(element: SendPicElement) {
super(element); 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<typeof Elem>[] {
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<SendFileElement> { export class PacketMsgVideoElement extends IPacketMsgElement<SendVideoElement> {
constructor(element: SendFileElement) { fileSize?: string;
filePath?: string;
thumbSize?: number;
thumbPath?: string;
fileMd5?: string;
fileSha1?: string;
thumbMd5?: string;
thumbSha1?: string;
thumbWidth?: number;
thumbHeight?: number;
msgInfo: NapProtoEncodeStructType<typeof MsgInfo> | null = null;
constructor(element: SendVideoElement) {
super(element); 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<typeof Elem>[] {
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<SendPttElement> { export class PacketMsgPttElement extends IPacketMsgElement<SendPttElement> {
filePath: string;
fileSize: number;
fileMd5: string;
fileSha1?: string;
fileDuration: number;
msgInfo: NapProtoEncodeStructType<typeof MsgInfo> | null = null;
constructor(element: SendPttElement) { constructor(element: SendPttElement) {
super(element); 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<typeof Elem>[] {
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<SendFileElement> {
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<typeof OidbSvcTrpcTcp0XE37_800Response>
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<typeof Elem>[] {
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}`;
} }
} }

View File

@ -1,6 +1,6 @@
import * as zlib from "node:zlib"; import * as zlib from "node:zlib";
import * as crypto from "node:crypto"; 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 { NapProtoMsg } from "@/core/packet/proto/NapProto";
import { OidbSvcTrpcTcpBase } from "@/core/packet/proto/oidb/OidbBase"; import { OidbSvcTrpcTcpBase } from "@/core/packet/proto/oidb/OidbBase";
import { OidbSvcTrpcTcp0X9067_202 } from "@/core/packet/proto/oidb/Oidb.0x9067_202"; 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 { HttpConn0x6ff_501 } from "@/core/packet/proto/action/action";
import { LongMsgResult, SendLongMsgReq } from "@/core/packet/proto/message/action"; import { LongMsgResult, SendLongMsgReq } from "@/core/packet/proto/message/action";
import { PacketMsgBuilder } from "@/core/packet/msg/builder"; 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 { LogWrapper } from "@/common/log";
import { PacketMsg } from "@/core/packet/msg/message"; import { PacketMsg } from "@/core/packet/msg/message";
import { OidbSvcTrpcTcp0x6D6 } from "@/core/packet/proto/oidb/Oidb.0x6D6"; import { OidbSvcTrpcTcp0x6D6 } from "@/core/packet/proto/oidb/Oidb.0x6D6";
import { OidbSvcTrpcTcp0XE37_1200 } from "@/core/packet/proto/oidb/Oidb.0xE37_1200"; import { OidbSvcTrpcTcp0XE37_1200 } from "@/core/packet/proto/oidb/Oidb.0xE37_1200";
import { PacketMsgConverter } from "@/core/packet/msg/converter"; import { PacketMsgConverter } from "@/core/packet/msg/converter";
import { PacketClient } from "@/core/packet/client"; 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 type PacketHexStr = string & { readonly hexNya: unique symbol };
export interface OidbPacket {
cmd: string;
data: PacketHexStr
}
export class PacketPacker { export class PacketPacker {
readonly logger: LogWrapper; readonly logger: LogWrapper;
readonly client: PacketClient; readonly client: PacketClient;
@ -34,30 +47,34 @@ export class PacketPacker {
this.packetConverter = new PacketMsgConverter(logger); this.packetConverter = new PacketMsgConverter(logger);
} }
private toHexStr(byteArray: Uint8Array): PacketHexStr { private packetPacket(byteArray: Uint8Array): PacketHexStr {
return Buffer.from(byteArray).toString('hex') as PacketHexStr; return Buffer.from(byteArray).toString('hex') as PacketHexStr;
} }
packOidbPacket(cmd: number, subCmd: number, body: Uint8Array, isUid: boolean = true, isLafter: boolean = false): Uint8Array { packOidbPacket(cmd: number, subCmd: number, body: Uint8Array, isUid: boolean = true, isLafter: boolean = false): OidbPacket {
return new NapProtoMsg(OidbSvcTrpcTcpBase).encode({ const data = new NapProtoMsg(OidbSvcTrpcTcpBase).encode({
command: cmd, command: cmd,
subCommand: subCmd, subCommand: subCmd,
body: body, body: body,
isReserved: isUid ? 1 : 0 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({ const oidb_0xed3 = new NapProtoMsg(OidbSvcTrpcTcp0XED3_1).encode({
uin: peer, uin: peer,
groupUin: group, groupUin: group,
friendUin: group ?? peer, friendUin: group ?? peer,
ext: 0 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({ const oidb_0x9067_202 = new NapProtoMsg(OidbSvcTrpcTcp0X9067_202).encode({
reqHead: { reqHead: {
common: { common: {
@ -77,10 +94,10 @@ export class PacketPacker {
key: [10, 20, 2] 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({ const oidb_0x8FC_2_body = new NapProtoMsg(OidbSvcTrpcTcp0X8FC_2_Body).encode({
targetUid: uid, targetUid: uid,
specialTitle: tittle, specialTitle: tittle,
@ -91,15 +108,15 @@ export class PacketPacker {
groupUin: +groupCode, groupUin: +groupCode,
body: oidb_0x8FC_2_body 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({ const oidb_0xfe1_2 = new NapProtoMsg(OidbSvcTrpcTcp0XFE1_2).encode({
uin: uin, 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<PacketHexStr> { async packUploadForwardMsg(selfUid: string, msg: PacketMsg[], groupUin: number = 0): Promise<PacketHexStr> {
@ -131,12 +148,12 @@ export class PacketPacker {
} }
); );
// this.logger.logDebug("packUploadForwardMsg REQ!!!", req); // this.logger.logDebug("packUploadForwardMsg REQ!!!", req);
return this.toHexStr(req); return this.packetPacket(req);
} }
// highway part // highway part
packHttp0x6ff_501(): PacketHexStr { packHttp0x6ff_501(): PacketHexStr {
return this.toHexStr(new NapProtoMsg(HttpConn0x6ff_501).encode({ return this.packetPacket(new NapProtoMsg(HttpConn0x6ff_501).encode({
httpConn: { httpConn: {
field1: 0, field1: 0,
field2: 0, field2: 0,
@ -153,7 +170,7 @@ export class PacketPacker {
})); }));
} }
async packUploadGroupImgReq(groupUin: number, img: PacketMsgPicElement): Promise<PacketHexStr> { async packUploadGroupImgReq(groupUin: number, img: PacketMsgPicElement): Promise<OidbPacket> {
const req = new NapProtoMsg(NTV2RichMediaReq).encode( const req = new NapProtoMsg(NTV2RichMediaReq).encode(
{ {
reqHead: { reqHead: {
@ -177,9 +194,9 @@ export class PacketPacker {
uploadInfo: [ uploadInfo: [
{ {
fileInfo: { fileInfo: {
fileSize: Number(img.size), fileSize: +img.size,
fileHash: img.md5, fileHash: img.md5,
fileSha1: this.toHexStr(await calculateSha1(img.path)), fileSha1: img.sha1!,
fileName: img.name, fileName: img.name,
type: { type: {
type: 1, 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<PacketHexStr> { async packUploadC2CImgReq(peerUin: string, img: PacketMsgPicElement): Promise<OidbPacket> {
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<OidbPacket> {
if (!video.fileSize || !video.thumbSize) throw new Error("video.fileSize or video.thumbSize is empty");
const req = new NapProtoMsg(NTV2RichMediaReq).encode({ const req = new NapProtoMsg(NTV2RichMediaReq).encode({
reqHead: { reqHead: {
common: { common: {
requestId: 1, requestId: 3,
command: 100 command: 100
}, },
scene: { scene: {
requestType: 2, requestType: 2,
businessType: 1, businessType: 2,
sceneType: 1, sceneType: 2,
c2C: { group: {
accountType: 2, groupUin: groupUin
targetUid: peerUin
}, },
}, },
client: { client: {
agentType: 2, agentType: 2
} }
}, },
upload: { upload: {
uploadInfo: [ uploadInfo: [
{ {
fileInfo: { fileInfo: {
fileSize: Number(img.size), fileSize: +video.fileSize,
fileHash: img.md5, fileHash: video.fileMd5,
fileSha1: this.toHexStr(await calculateSha1(img.path)), fileSha1: video.fileSha1,
fileName: img.name, 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: {
type: 1, type: 1,
picFormat: img.picType, //TODO: extend NapCat imgType /cc @MliKiowa picFormat: 0,
videoFormat: 0, videoFormat: 0,
voiceFormat: 0, voiceFormat: 0
}, },
width: img.width, height: video.thumbHeight,
height: img.height, width: video.thumbWidth,
time: 0, time: 0,
original: 1 original: 0
}, },
subFileType: 0, subFileType: 100
} }
], ],
tryFastUploadCompleted: true, tryFastUploadCompleted: true,
srvSendMsg: false, srvSendMsg: false,
clientRandomId: crypto.randomBytes(8).readBigUInt64BE() & BigInt('0x7FFFFFFFFFFFFFFF'), clientRandomId: crypto.randomBytes(8).readBigUInt64BE() & BigInt('0x7FFFFFFFFFFFFFFF'),
compatQMsgSceneType: 1, compatQMsgSceneType: 2,
extBizInfo: { extBizInfo: {
pic: { pic: {
bytesPbReserveTroop: Buffer.from("0800180020004200500062009201009a0100a2010c080012001800200028003a00", 'hex'), bizType: 0,
textSummary: "Nya~", // TODO: textSummary: "Nya~",
}, },
video: { video: {
bytesPbReserve: Buffer.alloc(0), bytesPbReserve: Buffer.from([0x80, 0x01, 0x00]),
}, },
ptt: { ptt: {
bytesPbReserve: Buffer.alloc(0), bytesPbReserve: Buffer.alloc(0),
@ -282,28 +385,297 @@ export class PacketPacker {
} }
}, },
clientSeq: 0, clientSeq: 0,
noNeedCompatMsg: false, noNeedCompatMsg: false
} }
} });
); return this.packOidbPacket(0x11EA, 100, req, true, false);
return this.toHexStr(this.packOidbPacket(0x11c5, 100, req, true, false));
} }
packGroupFileDownloadReq(groupUin: number, fileUUID: string): PacketHexStr { async packUploadC2CVideoReq(peerUin: string, video: PacketMsgVideoElement): Promise<OidbPacket> {
return this.toHexStr( if (!video.fileSize || !video.thumbSize) throw new Error("video.fileSize or video.thumbSize is empty");
this.packOidbPacket(0x6D6, 2, new NapProtoMsg(OidbSvcTrpcTcp0x6D6).encode({ 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<OidbPacket> {
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<OidbPacket> {
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<OidbPacket> {
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<OidbPacket> {
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: { download: {
groupUin: groupUin, groupUin: groupUin,
appId: 7, appId: 7,
busId: 102, busId: 102,
fileId: fileUUID fileId: fileUUID
} }
}), true, false) }), true, false
); );
} }
packC2CFileDownloadReq(selfUid: string, fileUUID: string, fileHash: string): PacketHexStr { packC2CFileDownloadReq(selfUid: string, fileUUID: string, fileHash: string): PacketHexStr {
return this.toHexStr( return this.packetPacket(
new NapProtoMsg(OidbSvcTrpcTcp0XE37_1200).encode({ new NapProtoMsg(OidbSvcTrpcTcp0XE37_1200).encode({
subCommand: 1200, subCommand: 1200,
field2: 1, 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);
}
} }

View File

@ -113,8 +113,26 @@ export const Permission = {
export const FileExtra = { export const FileExtra = {
file: ProtoField(1, () => NotOnlineFile), 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 = { export const GroupFileExtra = {
field1: ProtoField(1, ScalarType.UINT32), field1: ProtoField(1, ScalarType.UINT32),
fileName: ProtoField(2, ScalarType.STRING), fileName: ProtoField(2, ScalarType.STRING),
@ -132,8 +150,9 @@ export const GroupFileExtraInfo = {
fileSize: ProtoField(3, ScalarType.UINT64), fileSize: ProtoField(3, ScalarType.UINT64),
fileName: ProtoField(4, ScalarType.STRING), fileName: ProtoField(4, ScalarType.STRING),
field5: ProtoField(5, ScalarType.UINT32), field5: ProtoField(5, ScalarType.UINT32),
field7: ProtoField(7, ScalarType.STRING), fileSha: ProtoField(6, ScalarType.BYTES),
fileMd5: ProtoField(8, ScalarType.STRING), extInfoString: ProtoField(7, ScalarType.STRING),
fileMd5: ProtoField(8, ScalarType.BYTES),
}; };
export const ImageExtraUrl = { export const ImageExtraUrl = {

View File

@ -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),
}

View File

@ -30,7 +30,7 @@ export const OidbSvcTrpcTcp0XE37_1200ResponseBody = {
field10: ProtoField(10, ScalarType.UINT32, true), field10: ProtoField(10, ScalarType.UINT32, true),
state: ProtoField(20, ScalarType.STRING, true), state: ProtoField(20, ScalarType.STRING, true),
result: ProtoField(30, () => OidbSvcTrpcTcp0XE37_1200Result, true), result: ProtoField(30, () => OidbSvcTrpcTcp0XE37_1200Result, true),
metadata: ProtoField(40, () => OidbSvcTrpcTcp0XE37_1200Metadata, true), metadata: ProtoField(40, () => OidbSvcTrpcTcp0XE37_800_1200Metadata, true),
}; };
export const OidbSvcTrpcTcp0XE37_1200Result = { export const OidbSvcTrpcTcp0XE37_1200Result = {
@ -43,7 +43,7 @@ export const OidbSvcTrpcTcp0XE37_1200Result = {
extra: ProtoField(120, ScalarType.BYTES, true), extra: ProtoField(120, ScalarType.BYTES, true),
}; };
export const OidbSvcTrpcTcp0XE37_1200Metadata = { export const OidbSvcTrpcTcp0XE37_800_1200Metadata = {
uin: ProtoField(1, ScalarType.UINT32, true), uin: ProtoField(1, ScalarType.UINT32, true),
field2: ProtoField(2, ScalarType.UINT32, true), field2: ProtoField(2, ScalarType.UINT32, true),
field3: ProtoField(3, ScalarType.UINT32, true), field3: ProtoField(3, ScalarType.UINT32, true),

View File

@ -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),
}

View File

@ -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),
}

View File

@ -2,6 +2,7 @@
import * as crypto from 'crypto'; import * as crypto from 'crypto';
import * as stream from 'stream'; import * as stream from 'stream';
import * as fs from 'fs'; import * as fs from 'fs';
import {CalculateStreamBytesTransform} from "@/core/packet/utils/crypto/sha1StreamBytesTransform";
function sha1Stream(readable: stream.Readable) { function sha1Stream(readable: stream.Readable) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -10,7 +11,37 @@ function sha1Stream(readable: stream.Readable) {
}) as Promise<Buffer>; }) as Promise<Buffer>;
} }
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<Buffer>;
}
export function calculateSha1(filePath: string): Promise<Buffer> { export function calculateSha1(filePath: string): Promise<Buffer> {
const readable = fs.createReadStream(filePath); const readable = fs.createReadStream(filePath);
return sha1Stream(readable); return sha1Stream(readable);
} }
export function computeMd5AndLengthWithLimit(filePath: string, limit?: number): Promise<Buffer> {
const readStream = fs.createReadStream(filePath, limit ? { start: 0, end: limit - 1 } : {});
return md5Stream(readStream);
}
export function calculateSha1StreamBytes(filePath: string): Promise<Buffer[]> {
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);
});
}

View File

@ -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();

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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<typeof SchemaData>;
export class SetGroupSign extends BaseAction<Payload, any> {
actionName = ActionName.SetGroupSign;
payloadSchema = SchemaData;
async _handle(payload: Payload) {
return await this.core.apis.PacketApi.sendGroupSignPacket(payload.group_id);
}
}

View File

@ -93,6 +93,7 @@ import { GetGroupFileUrl } from "@/onebot/action/file/GetGroupFileUrl";
import { GetPacketStatus } from "@/onebot/action/packet/GetPacketStatus"; import { GetPacketStatus } from "@/onebot/action/packet/GetPacketStatus";
import { FriendPoke } from "@/onebot/action/user/FriendPoke"; import { FriendPoke } from "@/onebot/action/user/FriendPoke";
import { GetCredentials } from './system/GetCredentials'; import { GetCredentials } from './system/GetCredentials';
import { SetGroupSign } from './extends/SetGroupSign';
export type ActionMap = Map<string, BaseAction<any, any>>; export type ActionMap = Map<string, BaseAction<any, any>>;
@ -115,6 +116,7 @@ export function createActionMap(obContext: NapCatOneBot11Adapter, core: NapCatCo
new SetQQAvatar(obContext, core), new SetQQAvatar(obContext, core),
new TranslateEnWordToZn(obContext, core), new TranslateEnWordToZn(obContext, core),
new GetGroupRootFiles(obContext, core), new GetGroupRootFiles(obContext, core),
new SetGroupSign(obContext, core),
// onebot11 // onebot11
new SendLike(obContext, core), new SendLike(obContext, core),
new GetMsg(obContext, core), new GetMsg(obContext, core),

View File

@ -117,7 +117,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
if (getSpecialMsgNum(payload, OB11MessageDataType.node)) { if (getSpecialMsgNum(payload, OB11MessageDataType.node)) {
const packetMode = this.core.apis.PacketApi.available; const packetMode = this.core.apis.PacketApi.available;
const returnMsgAndResId = packetMode 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[]); : await this.handleForwardedNodes(peer, messages as OB11MessageNode[]);
if (returnMsgAndResId.message) { if (returnMsgAndResId.message) {
const msgShortId = MessageUnique.createUniqueMsgId({ const msgShortId = MessageUnique.createUniqueMsgId({
@ -146,7 +146,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
} }
// TODO: recursively handle forwarded nodes // 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, message: RawMessage | null,
res_id?: string res_id?: string
}> { }> {
@ -157,7 +157,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
const OB11Data = normalize(node.data.content); const OB11Data = normalize(node.data.content);
const { sendElements } = await this.obContext.apis.MsgApi.createSendElements(OB11Data, msgPeer); const { sendElements } = await this.obContext.apis.MsgApi.createSendElements(OB11Data, msgPeer);
const packetMsgElements: rawMsgWithSendMsg = { 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, senderName: node.data.nickname,
groupId: msgPeer.chatType === ChatType.KCHATTYPEGROUP ? +msgPeer.peerUid : undefined, groupId: msgPeer.chatType === ChatType.KCHATTYPEGROUP ? +msgPeer.peerUid : undefined,
time: Date.now(), time: Date.now(),
@ -172,7 +172,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
} }
} }
const resid = await this.core.apis.PacketApi.sendUploadForwardMsg(packetMsg, msgPeer.chatType === ChatType.KCHATTYPEGROUP ? +msgPeer.peerUid : 0); 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 = { const finallySendElements = {
elementType: ElementType.ARK, elementType: ElementType.ARK,
elementId: "", elementId: "",

View File

@ -134,5 +134,7 @@ export enum ActionName {
GetGuildProfile = 'get_guild_service_profile', GetGuildProfile = 'get_guild_service_profile',
GetGroupIgnoredNotifies = 'get_group_ignored_notifies', GetGroupIgnoredNotifies = 'get_group_ignored_notifies',
SetGroupSign = "set_group_sign",
// UploadForwardMsg = "upload_forward_msg", // UploadForwardMsg = "upload_forward_msg",
} }

View File

@ -16,8 +16,8 @@ export interface OB11Message {
message_id: number, message_id: number,
message_seq: number, // 和message_id一样 message_seq: number, // 和message_id一样
real_id: number, real_id: number,
user_id: number, user_id: number | string, // number
group_id?: number, group_id?: number | string, // number
message_type: 'private' | 'group', message_type: 'private' | 'group',
sub_type?: 'friend' | 'group' | 'normal', sub_type?: 'friend' | 'group' | 'normal',
sender: OB11Sender, sender: OB11Sender,
@ -149,7 +149,7 @@ export interface OB11MessageNode {
type: OB11MessageDataType.node; type: OB11MessageDataType.node;
data: { data: {
id?: string id?: string
user_id?: number user_id?: number | string // number
nickname: string nickname: string
content: OB11MessageMixType content: OB11MessageMixType
}; };
@ -221,6 +221,10 @@ export interface OB11PostSendMsg {
message: OB11MessageMixType; message: OB11MessageMixType;
messages?: OB11MessageMixType; // 兼容 go-cqhttp messages?: OB11MessageMixType; // 兼容 go-cqhttp
auto_escape?: boolean | string auto_escape?: boolean | string
source?: string,
news?: { text: string }[],
summary?: string,
prompt?: string
} }
export interface OB11PostContext { export interface OB11PostContext {
message_type?: 'private' | 'group' message_type?: 'private' | 'group'

View File

@ -30,7 +30,7 @@ async function onSettingWindowCreated(view: Element) {
SettingItem( SettingItem(
'<span id="napcat-update-title">Napcat</span>', '<span id="napcat-update-title">Napcat</span>',
undefined, undefined,
SettingButton('V3.1.3', 'napcat-update-button', 'secondary'), SettingButton('V3.1.5', 'napcat-update-button', 'secondary'),
), ),
]), ]),
SettingList([ SettingList([

View File

@ -164,7 +164,7 @@ async function onSettingWindowCreated(view) {
SettingItem( SettingItem(
'<span id="napcat-update-title">Napcat</span>', '<span id="napcat-update-title">Napcat</span>',
void 0, void 0,
SettingButton("V3.1.3", "napcat-update-button", "secondary") SettingButton("V3.1.5", "napcat-update-button", "secondary")
) )
]), ]),
SettingList([ SettingList([