mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2024-11-21 09:36:35 +00:00
Merge branch 'main' into laana
# Conflicts: # package.json
This commit is contained in:
commit
491db5b209
@ -1,5 +1,7 @@
|
||||
<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" />
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
@ -4,7 +4,7 @@
|
||||
"name": "NapCatQQ",
|
||||
"slug": "NapCat.Framework",
|
||||
"description": "高性能的 OneBot 11 协议实现",
|
||||
"version": "3.1.3",
|
||||
"version": "3.1.5",
|
||||
"icon": "./logo.png",
|
||||
"authors": [
|
||||
{
|
||||
|
@ -2,7 +2,7 @@
|
||||
"name": "napcat",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"version": "3.1.3",
|
||||
"version": "3.1.5",
|
||||
"scripts": {
|
||||
"build:framework": "vite build --mode framework",
|
||||
"build:shell": "vite build --mode shell",
|
||||
@ -21,7 +21,6 @@
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/fluent-ffmpeg": "^2.1.24",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/node": "^22.0.1",
|
||||
"@types/qrcode-terminal": "^0.12.2",
|
||||
"@types/ws": "^8.5.12",
|
||||
@ -37,9 +36,8 @@
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"fast-xml-parser": "^4.3.6",
|
||||
"file-type": "^19.0.0",
|
||||
"fluent-ffmpeg": "^2.1.2",
|
||||
"image-size": "^1.1.1",
|
||||
"json-schema-to-ts": "^3.1.0",
|
||||
"json-schema-to-ts": "^3.1.1",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "5.4.6",
|
||||
"vite-plugin-cp": "^4.0.8",
|
||||
|
@ -50,9 +50,29 @@ interface ForwardAdaptMsgElement {
|
||||
}
|
||||
|
||||
export class ForwardMsgBuilder {
|
||||
private static build(resId: string, msg: ForwardAdaptMsg[]): ForwardMsgJson {
|
||||
private static build(resId: string, msg: ForwardAdaptMsg[], source?: string, news?: ForwardMsgJsonMetaDetail["news"], summary?: string, prompt?: string): ForwardMsgJson {
|
||||
const id = crypto.randomUUID();
|
||||
const isGroupMsg = msg.some(m => m.isGroupMsg);
|
||||
if (!source) {
|
||||
source = isGroupMsg ? "群聊的聊天记录" :
|
||||
msg.length
|
||||
? Array.from(new Set(msg.map(m => m.senderName)))
|
||||
.join('和') + '的聊天记录'
|
||||
: '聊天记录';
|
||||
}
|
||||
if (!news) {
|
||||
news = msg.length === 0 ? [{
|
||||
text: "Nya~ This message is send from NapCat.Packet!",
|
||||
}] : msg.map(m => ({
|
||||
text: `${m.senderName}: ${m.msg?.map(msg => msg.preview).join('')}`,
|
||||
}));
|
||||
}
|
||||
if (!summary) {
|
||||
summary = `查看${msg.length}条转发消息`;
|
||||
}
|
||||
if (!prompt) {
|
||||
prompt = "[聊天记录]";
|
||||
}
|
||||
return {
|
||||
app: "com.tencent.multimsg",
|
||||
config: {
|
||||
@ -62,29 +82,21 @@ export class ForwardMsgBuilder {
|
||||
type: "normal",
|
||||
width: 300
|
||||
},
|
||||
desc: "[聊天记录]",
|
||||
desc: prompt,
|
||||
extra: {
|
||||
filename: id,
|
||||
tsum: msg.length,
|
||||
},
|
||||
meta: {
|
||||
detail: {
|
||||
news: msg.length === 0 ? [{
|
||||
text: "Nya~ This message is send from NapCat.Packet!",
|
||||
}] : msg.map(m => ({
|
||||
text: `${m.senderName}: ${m.msg?.map(msg => msg.preview).join('')}`,
|
||||
})),
|
||||
news,
|
||||
resid: resId,
|
||||
source: isGroupMsg ? "群聊的聊天记录" :
|
||||
msg.length
|
||||
? Array.from(new Set(msg.map(m => m.senderName)))
|
||||
.join('和') + '的聊天记录'
|
||||
: '聊天记录',
|
||||
summary: `查看${msg.length}条转发消息`,
|
||||
source,
|
||||
summary,
|
||||
uniseq: id,
|
||||
}
|
||||
},
|
||||
prompt: "[聊天记录]",
|
||||
prompt,
|
||||
ver: "0.0.0.5",
|
||||
view: "contact",
|
||||
};
|
||||
@ -94,13 +106,13 @@ export class ForwardMsgBuilder {
|
||||
return this.build(resId, []);
|
||||
}
|
||||
|
||||
static fromPacketMsg(resId: string, packetMsg: PacketMsg[]): ForwardMsgJson {
|
||||
static fromPacketMsg(resId: string, packetMsg: PacketMsg[], source?: string, news?: ForwardMsgJsonMetaDetail["news"], summary?: string, prompt?: string): ForwardMsgJson {
|
||||
return this.build(resId, packetMsg.map(msg => ({
|
||||
senderName: msg.senderName,
|
||||
isGroupMsg: msg.groupId !== undefined,
|
||||
msg: msg.msg.map(m => ({
|
||||
preview: m.toPreview(),
|
||||
preview: m.valid? m.toPreview() : "[该消息类型暂不支持查看]",
|
||||
}))
|
||||
})));
|
||||
})), source, news, summary, prompt);
|
||||
}
|
||||
}
|
||||
|
@ -1 +1 @@
|
||||
export const napCatVersion = '3.1.3';
|
||||
export const napCatVersion = '3.1.5';
|
||||
|
@ -3,7 +3,7 @@ import { ChatType, InstanceContext, NapCatCore } from '..';
|
||||
import offset from '@/core/external/offset.json';
|
||||
import { PacketClient, RecvPacketData } from '@/core/packet/client';
|
||||
import { PacketSession } from "@/core/packet/session";
|
||||
import { PacketHexStr } from "@/core/packet/packer";
|
||||
import {OidbPacket, PacketHexStr} from "@/core/packet/packer";
|
||||
import { NapProtoMsg } from '@/core/packet/proto/NapProto';
|
||||
import { OidbSvcTrpcTcp0X9067_202_Rsp_Body } from '@/core/packet/proto/oidb/Oidb.0x9067_202';
|
||||
import { OidbSvcTrpcTcpBase, OidbSvcTrpcTcpBaseRsp } from '@/core/packet/proto/oidb/OidbBase';
|
||||
@ -12,7 +12,12 @@ import { LogWrapper } from "@/common/log";
|
||||
import { SendLongMsgResp } from "@/core/packet/proto/message/action";
|
||||
import { PacketMsg } from "@/core/packet/msg/message";
|
||||
import { OidbSvcTrpcTcp0x6D6Response } from "@/core/packet/proto/oidb/Oidb.0x6D6";
|
||||
import { PacketMsgPicElement } from "@/core/packet/msg/element";
|
||||
import {
|
||||
PacketMsgFileElement,
|
||||
PacketMsgPicElement,
|
||||
PacketMsgPttElement,
|
||||
PacketMsgVideoElement
|
||||
} from "@/core/packet/msg/element";
|
||||
|
||||
|
||||
interface OffsetType {
|
||||
@ -73,25 +78,32 @@ export class NTQQPacketApi {
|
||||
return this.packetSession!.client.sendPacket(cmd, data, rsp);
|
||||
}
|
||||
|
||||
async sendOidbPacket(pkt: OidbPacket, rsp = false): Promise<RecvPacketData> {
|
||||
return this.sendPacket(pkt.cmd, pkt.data, rsp);
|
||||
}
|
||||
|
||||
async sendPokePacket(peer: number, group?: number) {
|
||||
const data = this.packetSession?.packer.packPokePacket(peer, group);
|
||||
await this.sendPacket('OidbSvcTrpcTcp.0xed3_1', data!, false);
|
||||
await this.sendOidbPacket(data!, false);
|
||||
}
|
||||
|
||||
async sendRkeyPacket() {
|
||||
const packet = this.packetSession?.packer.packRkeyPacket();
|
||||
const ret = await this.sendPacket('OidbSvcTrpcTcp.0x9067_202', packet!, true);
|
||||
const ret = await this.sendOidbPacket(packet!, true);
|
||||
if (!ret?.hex_data) return [];
|
||||
const body = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(Buffer.from(ret.hex_data, 'hex')).body;
|
||||
const retData = new NapProtoMsg(OidbSvcTrpcTcp0X9067_202_Rsp_Body).decode(body);
|
||||
return retData.data.rkeyList;
|
||||
}
|
||||
|
||||
async sendGroupSignPacket(groupCode: string) {
|
||||
const packet = this.packetSession?.packer.packGroupSignReq(this.core.selfInfo.uin, groupCode);
|
||||
await this.sendOidbPacket(packet!, true);
|
||||
}
|
||||
async sendStatusPacket(uin: number): Promise<{ status: number; ext_status: number; } | undefined> {
|
||||
let status = 0;
|
||||
try {
|
||||
const packet = this.packetSession?.packer.packStatusPacket(uin);
|
||||
const ret = await this.sendPacket('OidbSvcTrpcTcp.0xfe1_2', packet!, true);
|
||||
const ret = await this.sendOidbPacket( packet!, true);
|
||||
const data = Buffer.from(ret.hex_data, 'hex');
|
||||
const ext = new NapProtoMsg(OidbSvcTrpcTcp0XFE1_2RSP).decode(new NapProtoMsg(OidbSvcTrpcTcpBase).decode(data).body).data.status.value;
|
||||
// ext & 0xff00 + ext >> 16 & 0xff
|
||||
@ -108,22 +120,47 @@ export class NTQQPacketApi {
|
||||
|
||||
async sendSetSpecialTittlePacket(groupCode: string, uid: string, tittle: string) {
|
||||
const data = this.packetSession?.packer.packSetSpecialTittlePacket(groupCode, uid, tittle);
|
||||
await this.sendPacket('OidbSvcTrpcTcp.0x8fc_2', data!, true);
|
||||
await this.sendOidbPacket(data!, true);
|
||||
}
|
||||
|
||||
private async uploadResources(msg: PacketMsg[], groupUin: number = 0) {
|
||||
// TODO: can simplify this
|
||||
async uploadResources(msg: PacketMsg[], groupUin: number = 0) {
|
||||
const reqList = [];
|
||||
for (const m of msg) {
|
||||
for (const e of m.msg) {
|
||||
if (e instanceof PacketMsgPicElement) {
|
||||
reqList.push(this.packetSession?.highwaySession.uploadImage({
|
||||
chatType: groupUin ? ChatType.KCHATTYPEGROUP : ChatType.KCHATTYPEC2C,
|
||||
peerUid: String(groupUin) ? String(groupUin) : this.core.selfInfo.uid
|
||||
peerUid: groupUin ? String(groupUin) : this.core.selfInfo.uid
|
||||
}, e));
|
||||
}
|
||||
if (e instanceof PacketMsgVideoElement) {
|
||||
reqList.push(this.packetSession?.highwaySession.uploadVideo({
|
||||
chatType: groupUin ? ChatType.KCHATTYPEGROUP : ChatType.KCHATTYPEC2C,
|
||||
peerUid: groupUin ? String(groupUin) : this.core.selfInfo.uid
|
||||
}, e));
|
||||
}
|
||||
if (e instanceof PacketMsgPttElement) {
|
||||
reqList.push(this.packetSession?.highwaySession.uploadPtt({
|
||||
chatType: groupUin ? ChatType.KCHATTYPEGROUP : ChatType.KCHATTYPEC2C,
|
||||
peerUid: groupUin ? String(groupUin) : this.core.selfInfo.uid
|
||||
}, e));
|
||||
}
|
||||
if (e instanceof PacketMsgFileElement){
|
||||
reqList.push(this.packetSession?.highwaySession.uploadFile({
|
||||
chatType: groupUin ? ChatType.KCHATTYPEGROUP : ChatType.KCHATTYPEC2C,
|
||||
peerUid: groupUin ? String(groupUin) : this.core.selfInfo.uid
|
||||
}, e));
|
||||
}
|
||||
}
|
||||
}
|
||||
return Promise.all(reqList);
|
||||
const res = await Promise.allSettled(reqList);
|
||||
this.logger.log(`上传资源${res.length}个, 失败${res.filter(r => r.status === 'rejected').length}个`);
|
||||
res.forEach((result, index) => {
|
||||
if (result.status === 'rejected') {
|
||||
this.logger.logError(`第${index + 1}个失败:${result.reason}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async sendUploadForwardMsg(msg: PacketMsg[], groupUin: number = 0) {
|
||||
@ -137,7 +174,7 @@ export class NTQQPacketApi {
|
||||
|
||||
async sendGroupFileDownloadReq(groupUin: number, fileUUID: string) {
|
||||
const data = this.packetSession?.packer.packGroupFileDownloadReq(groupUin, fileUUID);
|
||||
const ret = await this.sendPacket('OidbSvcTrpcTcp.0x6d6_2', data!, true);
|
||||
const ret = await this.sendOidbPacket(data!, true);
|
||||
const body = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(Buffer.from(ret.hex_data, 'hex')).body;
|
||||
const resp = new NapProtoMsg(OidbSvcTrpcTcp0x6D6Response).decode(body);
|
||||
if (resp.download.retCode !== 0) {
|
||||
|
8
src/core/external/appid.json
vendored
8
src/core/external/appid.json
vendored
@ -50,5 +50,13 @@
|
||||
"9.9.16-28788": {
|
||||
"appid": 537249739,
|
||||
"qua": "V1_WIN_NQ_9.9.16_28788_GW_B"
|
||||
},
|
||||
"9.9.16-28971":{
|
||||
"appid": 537249775,
|
||||
"qua": "V1_WIN_NQ_9.9.16_28971_GW_B"
|
||||
},
|
||||
"3.2.13-28971": {
|
||||
"appid": 537249848,
|
||||
"qua": "V1_LNX_NQ_3.2.13_28971_GW_B"
|
||||
}
|
||||
}
|
14
src/core/external/offset.json
vendored
14
src/core/external/offset.json
vendored
@ -19,8 +19,20 @@
|
||||
"send": "A0CEC20",
|
||||
"recv": "A0D2520"
|
||||
},
|
||||
"3.2.13-28788-arm64":{
|
||||
"3.2.13-28788-arm64": {
|
||||
"send": "6E91018",
|
||||
"recv": "6E94850"
|
||||
},
|
||||
"9.9.16-28971-x64": {
|
||||
"send": "38079F0",
|
||||
"recv": "380BE24"
|
||||
},
|
||||
"3.2.13-28971-x64": {
|
||||
"send": "A0CEF60",
|
||||
"recv": "A0D2860"
|
||||
},
|
||||
"3.2.12-28971-arm64": {
|
||||
"send": "6E91318",
|
||||
"recv": "6E94B50"
|
||||
}
|
||||
}
|
@ -3,8 +3,7 @@ import { LRUCache } from "@/common/lru-cache";
|
||||
import WebSocket, { Data } from "ws";
|
||||
import crypto, { createHash } from "crypto";
|
||||
import { NapCatCore } from "@/core";
|
||||
import { PacketHexStr } from "@/core/packet/packer";
|
||||
import { sleep } from "@/common/helper";
|
||||
import { OidbPacket, PacketHexStr } from "@/core/packet/packer";
|
||||
|
||||
export interface RecvPacket {
|
||||
type: string, // 仅recv
|
||||
@ -177,4 +176,8 @@ export class PacketClient {
|
||||
}).then((res) => resolve(res)).catch((e: Error) => reject(e));
|
||||
});
|
||||
}
|
||||
|
||||
async sendOidbPacket(pkt: OidbPacket, rsp = false): Promise<RecvPacketData> {
|
||||
return this.sendPacket(pkt.cmd, pkt.data, rsp);
|
||||
}
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ export class PacketHighwayClient {
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
private buildDataUpTrans(cmd: number, data: ReadStream, fileSize: number, md5: Uint8Array, extendInfo: Uint8Array, timeout: number = 3600): PacketHighwayTrans {
|
||||
private buildDataUpTrans(cmd: number, data: ReadStream, fileSize: number, md5: Uint8Array, extendInfo: Uint8Array, timeout: number = 1200): PacketHighwayTrans {
|
||||
return {
|
||||
uin: this.sig.uin,
|
||||
cmd: cmd,
|
||||
|
@ -8,9 +8,17 @@ import { HttpConn0x6ff_501Response } from "@/core/packet/proto/action/action";
|
||||
import { PacketHighwayClient } from "@/core/packet/highway/client";
|
||||
import { NTV2RichMediaResp } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaResp";
|
||||
import { OidbSvcTrpcTcpBaseRsp } from "@/core/packet/proto/oidb/OidbBase";
|
||||
import { PacketMsgPicElement } from "@/core/packet/msg/element";
|
||||
import { NTV2RichMediaHighwayExt } from "@/core/packet/proto/highway/highway";
|
||||
import {
|
||||
PacketMsgFileElement,
|
||||
PacketMsgPicElement,
|
||||
PacketMsgPttElement,
|
||||
PacketMsgVideoElement
|
||||
} from "@/core/packet/msg/element";
|
||||
import { FileUploadExt, NTV2RichMediaHighwayExt } from "@/core/packet/proto/highway/highway";
|
||||
import { int32ip2str, oidbIpv4s2HighwayIpv4s } from "@/core/packet/highway/utils";
|
||||
import { calculateSha1, calculateSha1StreamBytes, computeMd5AndLengthWithLimit } from "@/core/packet/utils/crypto/hash";
|
||||
import { OidbSvcTrpcTcp0x6D6Response } from "@/core/packet/proto/oidb/Oidb.0x6D6";
|
||||
import { OidbSvcTrpcTcp0XE37_800Response, OidbSvcTrpcTcp0XE37Response } from "@/core/packet/proto/oidb/Oidb.0XE37_800";
|
||||
|
||||
export const BlockSize = 1024 * 1024;
|
||||
|
||||
@ -21,6 +29,7 @@ interface HighwayServerAddr {
|
||||
|
||||
export interface PacketHighwaySig {
|
||||
uin: string;
|
||||
uid: string;
|
||||
sigSession: Uint8Array | null
|
||||
sessionKey: Uint8Array | null
|
||||
serverAddr: HighwayServerAddr[]
|
||||
@ -39,6 +48,7 @@ export class PacketHighwaySession {
|
||||
this.logger = logger;
|
||||
this.sig = {
|
||||
uin: this.packetClient.napCatCore.selfInfo.uin,
|
||||
uid: this.packetClient.napCatCore.selfInfo.uid,
|
||||
sigSession: null,
|
||||
sessionKey: null,
|
||||
serverAddr: [],
|
||||
@ -49,7 +59,6 @@ export class PacketHighwaySession {
|
||||
|
||||
private async checkAvailable() {
|
||||
if (!this.packetClient.available) {
|
||||
this.logger.logError('[Highway] packetServer not available!');
|
||||
throw new Error('packetServer不可用,请参照文档 https://napneko.github.io/config/advanced 检查packetServer状态或进行配置');
|
||||
}
|
||||
if (this.sig.sigSession === null || this.sig.sessionKey === null) {
|
||||
@ -86,7 +95,7 @@ export class PacketHighwaySession {
|
||||
async uploadImage(peer: Peer, img: PacketMsgPicElement): Promise<void> {
|
||||
await this.checkAvailable();
|
||||
if (peer.chatType === ChatType.KCHATTYPEGROUP) {
|
||||
await this.uploadGroupImageReq(Number(peer.peerUid), img);
|
||||
await this.uploadGroupImageReq(+peer.peerUid, img);
|
||||
} else if (peer.chatType === ChatType.KCHATTYPEC2C) {
|
||||
await this.uploadC2CImageReq(peer.peerUid, img);
|
||||
} else {
|
||||
@ -94,16 +103,53 @@ export class PacketHighwaySession {
|
||||
}
|
||||
}
|
||||
|
||||
async uploadVideo(peer: Peer, video: PacketMsgVideoElement): Promise<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> {
|
||||
img.sha1 = Buffer.from(await calculateSha1(img.path)).toString('hex');
|
||||
const preReq = await this.packer.packUploadGroupImgReq(groupUin, img);
|
||||
const preRespRaw = await this.packetClient.sendPacket('OidbSvcTrpcTcp.0x11c4_100', preReq, true);
|
||||
const preRespRaw = await this.packetClient.sendOidbPacket(preReq, true);
|
||||
const preResp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(
|
||||
Buffer.from(preRespRaw.hex_data, 'hex')
|
||||
);
|
||||
const preRespData = new NapProtoMsg(NTV2RichMediaResp).decode(preResp.body);
|
||||
const ukey = preRespData.upload.uKey;
|
||||
if (ukey && ukey != "") {
|
||||
this.logger.logDebug(`[Highway] get upload ukey: ${ukey}, need upload!`);
|
||||
this.logger.logDebug(`[Highway] uploadGroupImageReq get upload ukey: ${ukey}, need upload!`);
|
||||
const index = preRespData.upload.msgInfo.msgInfoBody[0].index;
|
||||
const sha1 = Buffer.from(index.info.fileSha1, 'hex');
|
||||
const md5 = Buffer.from(index.info.fileHash, 'hex');
|
||||
@ -121,28 +167,29 @@ export class PacketHighwaySession {
|
||||
});
|
||||
await this.packetHighwayClient.upload(
|
||||
1004,
|
||||
fs.createReadStream(img.path, { highWaterMark: BlockSize }),
|
||||
fs.createReadStream(img.path, {highWaterMark: BlockSize}),
|
||||
img.size,
|
||||
md5,
|
||||
extend
|
||||
);
|
||||
} else {
|
||||
this.logger.logDebug(`[Highway] get upload invalid ukey ${ukey}, don't need upload!`);
|
||||
this.logger.logDebug(`[Highway] uploadGroupImageReq get upload invalid ukey ${ukey}, don't need upload!`);
|
||||
}
|
||||
img.msgInfo = preRespData.upload.msgInfo;
|
||||
// img.groupPicExt = new NapProtoMsg(CustomFace).decode(preRespData.tcpUpload.compatQMsg)
|
||||
}
|
||||
|
||||
private async uploadC2CImageReq(peerUid: string, img: PacketMsgPicElement): Promise<void> {
|
||||
img.sha1 = Buffer.from(await calculateSha1(img.path)).toString('hex');
|
||||
const preReq = await this.packer.packUploadC2CImgReq(peerUid, img);
|
||||
const preRespRaw = await this.packetClient.sendPacket('OidbSvcTrpcTcp.0x11c5_100', preReq, true);
|
||||
const preRespRaw = await this.packetClient.sendOidbPacket(preReq, true);
|
||||
const preResp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(
|
||||
Buffer.from(preRespRaw.hex_data, 'hex')
|
||||
);
|
||||
const preRespData = new NapProtoMsg(NTV2RichMediaResp).decode(preResp.body);
|
||||
const ukey = preRespData.upload.uKey;
|
||||
if (ukey && ukey != "") {
|
||||
this.logger.logDebug(`[Highway] get upload ukey: ${ukey}, need upload!`);
|
||||
this.logger.logDebug(`[Highway] uploadC2CImageReq get upload ukey: ${ukey}, need upload!`);
|
||||
const index = preRespData.upload.msgInfo.msgInfoBody[0].index;
|
||||
const sha1 = Buffer.from(index.info.fileSha1, 'hex');
|
||||
const md5 = Buffer.from(index.info.fileHash, 'hex');
|
||||
@ -160,12 +207,364 @@ export class PacketHighwaySession {
|
||||
});
|
||||
await this.packetHighwayClient.upload(
|
||||
1003,
|
||||
fs.createReadStream(img.path, { highWaterMark: BlockSize }),
|
||||
fs.createReadStream(img.path, {highWaterMark: BlockSize}),
|
||||
img.size,
|
||||
md5,
|
||||
extend
|
||||
);
|
||||
} else {
|
||||
this.logger.logDebug(`[Highway] uploadC2CImageReq get upload invalid ukey ${ukey}, don't need upload!`);
|
||||
}
|
||||
img.msgInfo = preRespData.upload.msgInfo;
|
||||
}
|
||||
|
||||
private async uploadGroupVideoReq(groupUin: number, video: PacketMsgVideoElement): Promise<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;
|
||||
}
|
||||
}
|
||||
|
@ -19,11 +19,20 @@ abstract class HighwayUploader {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
encryptTransExt(key: Uint8Array) {
|
||||
private encryptTransExt(key: Uint8Array) {
|
||||
if (!this.trans.encrypt) return;
|
||||
this.trans.ext = tea.encrypt(Buffer.from(this.trans.ext), Buffer.from(key));
|
||||
}
|
||||
|
||||
protected timeout(): Promise<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 {
|
||||
return new NapProtoMsg(ReqDataHighwayHead).encode({
|
||||
msgBaseHead: {
|
||||
@ -86,15 +95,18 @@ class HighwayTcpUploaderTransform extends stream.Transform {
|
||||
|
||||
export class HighwayTcpUploader extends HighwayUploader {
|
||||
async upload(): Promise<void> {
|
||||
const controller = new AbortController();
|
||||
const { signal } = controller;
|
||||
const upload = new Promise<void>((resolve, reject) => {
|
||||
const highwayTransForm = new HighwayTcpUploaderTransform(this);
|
||||
const upload = new Promise<void>((resolve, _) => {
|
||||
const socket = net.connect(this.trans.port, this.trans.server, () => {
|
||||
this.trans.data.pipe(highwayTransForm).pipe(socket, { end: false });
|
||||
this.trans.data.pipe(highwayTransForm).pipe(socket, {end: false});
|
||||
});
|
||||
const handleRspHeader = (header: Buffer) => {
|
||||
const rsp = new NapProtoMsg(RespDataHighwayHead).decode(header);
|
||||
if (rsp.errorCode !== 0) {
|
||||
this.logger.logWarn(`[Highway] tcpUpload failed (code: ${rsp.errorCode})`);
|
||||
socket.end();
|
||||
reject(new Error(`[Highway] tcpUpload failed (code=${rsp.errorCode})`));
|
||||
}
|
||||
const percent = ((Number(rsp.msgSegHead?.dataOffset) + Number(rsp.msgSegHead?.dataLength)) / Number(rsp.msgSegHead?.filesize)).toFixed(2);
|
||||
this.logger.logDebug(`[Highway] tcpUpload ${rsp.errorCode} | ${percent} | ${Buffer.from(header).toString('hex')}`);
|
||||
@ -105,49 +117,58 @@ export class HighwayTcpUploader extends HighwayUploader {
|
||||
}
|
||||
};
|
||||
socket.on('data', (chunk: Buffer) => {
|
||||
try {
|
||||
if (signal.aborted) {
|
||||
socket.end();
|
||||
reject(new Error('Upload aborted due to timeout'));
|
||||
}
|
||||
const [head, _] = Frame.unpack(chunk);
|
||||
handleRspHeader(head);
|
||||
} catch (e) {
|
||||
this.logger.logError(`[Highway] tcpUpload parse response error: ${e}`);
|
||||
}
|
||||
});
|
||||
socket.on('close', () => {
|
||||
this.logger.logDebug('[Highway] tcpUpload socket closed.');
|
||||
resolve();
|
||||
});
|
||||
socket.on('error', (err) => {
|
||||
this.logger.logError('[Highway] tcpUpload socket.on error:', err);
|
||||
socket.end();
|
||||
reject(new Error(`[Highway] tcpUpload socket.on error: ${err}`));
|
||||
});
|
||||
this.trans.data.on('error', (err) => {
|
||||
this.logger.logError('[Highway] tcpUpload readable error:', err);
|
||||
socket.end();
|
||||
reject(new Error(`[Highway] tcpUpload readable error: ${err}`));
|
||||
});
|
||||
});
|
||||
const timeout = new Promise<void>((_, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(new Error(`[Highway] tcpUpload timeout after ${this.trans.timeout}s`));
|
||||
}, (this.trans.timeout ?? Infinity) * 1000
|
||||
);
|
||||
const timeout = this.timeout().catch((err) => {
|
||||
controller.abort();
|
||||
throw new Error(err.message);
|
||||
});
|
||||
await Promise.race([upload, timeout]);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: timeout impl
|
||||
export class HighwayHttpUploader extends HighwayUploader {
|
||||
async upload(): Promise<void> {
|
||||
const controller = new AbortController();
|
||||
const { signal } = controller;
|
||||
const upload = (async () => {
|
||||
let offset = 0;
|
||||
for await (const chunk of this.trans.data) {
|
||||
if (signal.aborted) {
|
||||
throw new Error('Upload aborted due to timeout');
|
||||
}
|
||||
const block = chunk as Buffer;
|
||||
try {
|
||||
await this.uploadBlock(block, offset);
|
||||
} catch (err) {
|
||||
this.logger.logError(`[Highway] httpUpload Error uploading block at offset ${offset}: ${err}`);
|
||||
throw err;
|
||||
throw new Error(`[Highway] httpUpload Error uploading block at offset ${offset}: ${err}`)
|
||||
}
|
||||
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> {
|
||||
@ -158,9 +179,7 @@ export class HighwayHttpUploader extends HighwayUploader {
|
||||
const [head, body] = Frame.unpack(resp);
|
||||
const headData = new NapProtoMsg(RespDataHighwayHead).decode(head);
|
||||
this.logger.logDebug(`[Highway] httpUploadBlock: ${headData.errorCode} | ${headData.msgSegHead?.retCode} | ${headData.bytesRspExtendInfo} | ${head.toString('hex')} | ${body.toString('hex')}`);
|
||||
if (headData.errorCode !== 0) {
|
||||
this.logger.logError(`[Highway] httpUploadBlock failed (code=${headData.errorCode})`);
|
||||
}
|
||||
if (headData.errorCode !== 0) throw new Error(`[Highway] httpUploadBlock failed (code=${headData.errorCode})`);
|
||||
}
|
||||
|
||||
private async httpPostHighwayContent(frame: Buffer, serverURL: string): Promise<Buffer> {
|
||||
@ -176,12 +195,12 @@ export class HighwayHttpUploader extends HighwayUploader {
|
||||
},
|
||||
};
|
||||
const req = http.request(serverURL, options, (res) => {
|
||||
let data = Buffer.alloc(0);
|
||||
const data: Buffer[] = [];
|
||||
res.on('data', (chunk) => {
|
||||
data = Buffer.concat([data, chunk]);
|
||||
data.push(chunk);
|
||||
});
|
||||
res.on('end', () => {
|
||||
resolve(data);
|
||||
resolve(Buffer.concat(data));
|
||||
});
|
||||
});
|
||||
req.write(frame);
|
||||
|
@ -2,7 +2,9 @@ import * as crypto from "crypto";
|
||||
import { PushMsgBody } from "@/core/packet/proto/message/message";
|
||||
import { NapProtoEncodeStructType } from "@/core/packet/proto/NapProto";
|
||||
import { LogWrapper } from "@/common/log";
|
||||
import { PacketMsg } from "@/core/packet/msg/message";
|
||||
import { PacketMsg, PacketSendMsgElement } from "@/core/packet/msg/message";
|
||||
import { IPacketMsgElement, PacketMsgTextElement } from "@/core/packet/msg/element";
|
||||
import { SendTextElement } from "@/core";
|
||||
|
||||
export class PacketMsgBuilder {
|
||||
private logger: LogWrapper;
|
||||
@ -11,10 +13,23 @@ export class PacketMsgBuilder {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
protected static failBackText = new PacketMsgTextElement(
|
||||
{
|
||||
textElement: {content: "[该消息类型暂不支持查看]"}!
|
||||
} as SendTextElement
|
||||
)
|
||||
|
||||
buildFakeMsg(selfUid: string, element: PacketMsg[]): NapProtoEncodeStructType<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 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() ?? []);
|
||||
if (!msgContent && !msgElement.length) {
|
||||
this.logger.logWarn(`[PacketMsgBuilder] buildFakeMsg: 空的msgContent和msgElement!`);
|
||||
msgElement.push(PacketMsgBuilder.failBackText.buildElement());
|
||||
}
|
||||
return {
|
||||
responseHead: {
|
||||
fromUid: "",
|
||||
@ -50,7 +65,8 @@ export class PacketMsgBuilder {
|
||||
body: {
|
||||
richText: {
|
||||
elems: msgElement
|
||||
}
|
||||
},
|
||||
msgContent: msgContent,
|
||||
}
|
||||
};
|
||||
});
|
||||
|
@ -1,4 +1,3 @@
|
||||
import assert from "node:assert";
|
||||
import * as zlib from "node:zlib";
|
||||
import { NapProtoEncodeStructType, NapProtoMsg } from "@/core/packet/proto/NapProto";
|
||||
import {
|
||||
@ -28,6 +27,8 @@ import {
|
||||
import { MsgInfo } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaReq";
|
||||
import { PacketMsg, PacketSendMsgElement } from "@/core/packet/msg/message";
|
||||
import { ForwardMsgBuilder } from "@/common/forward-msg-builder";
|
||||
import { FileExtra, GroupFileExtra } from "@/core/packet/proto/message/component";
|
||||
import { OidbSvcTrpcTcp0XE37_800Response } from "@/core/packet/proto/oidb/Oidb.0XE37_800";
|
||||
|
||||
// raw <-> packet
|
||||
// TODO: SendStructLongMsgElement
|
||||
@ -35,16 +36,20 @@ export abstract class IPacketMsgElement<T extends PacketSendMsgElement> {
|
||||
protected constructor(rawElement: T) {
|
||||
}
|
||||
|
||||
get valid(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
buildContent(): Uint8Array | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
buildElement(): NapProtoEncodeStructType<typeof Elem>[] | undefined {
|
||||
return undefined;
|
||||
buildElement(): NapProtoEncodeStructType<typeof Elem>[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
toPreview(): string {
|
||||
return '[nya~]';
|
||||
return '[暂不支持该消息类型喵~]';
|
||||
}
|
||||
}
|
||||
|
||||
@ -93,50 +98,6 @@ export class PacketMsgAtElement extends PacketMsgTextElement {
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
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> {
|
||||
@ -151,11 +112,11 @@ export class PacketMsgReplyElement extends IPacketMsgElement<SendReplyElement> {
|
||||
constructor(element: SendReplyElement) {
|
||||
super(element);
|
||||
this.messageId = BigInt(element.replyElement.replayMsgId ?? 0);
|
||||
this.messageSeq = Number(element.replyElement.replayMsgSeq ?? 0);
|
||||
this.messageClientSeq = Number(element.replyElement.replyMsgClientSeq ?? 0);
|
||||
this.targetUin = Number(element.replyElement.senderUin ?? 0);
|
||||
this.messageSeq = +(element.replyElement.replayMsgSeq ?? 0);
|
||||
this.messageClientSeq = +(element.replyElement.replyMsgClientSeq ?? 0);
|
||||
this.targetUin = +(element.replyElement.senderUin ?? 0);
|
||||
this.targetUid = element.replyElement.senderUidStr ?? '';
|
||||
this.time = Number(element.replyElement.replyMsgTime ?? 0);
|
||||
this.time = +(element.replyElement.replyMsgTime ?? 0);
|
||||
this.elems = []; // TODO: in replyElement.sourceMsgTextElems
|
||||
}
|
||||
|
||||
@ -189,7 +150,7 @@ export class PacketMsgReplyElement extends IPacketMsgElement<SendReplyElement> {
|
||||
}
|
||||
|
||||
toPreview(): string {
|
||||
return "[回复]";
|
||||
return "[回复消息]";
|
||||
}
|
||||
}
|
||||
|
||||
@ -284,21 +245,216 @@ export class PacketMsgMarkFaceElement extends IPacketMsgElement<SendMarketFaceEl
|
||||
}
|
||||
}
|
||||
|
||||
export class PacketMsgVideoElement extends IPacketMsgElement<SendVideoElement> {
|
||||
constructor(element: SendVideoElement) {
|
||||
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 = +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> {
|
||||
constructor(element: SendFileElement) {
|
||||
export class PacketMsgVideoElement extends IPacketMsgElement<SendVideoElement> {
|
||||
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);
|
||||
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> {
|
||||
filePath: string;
|
||||
fileSize: number;
|
||||
fileMd5: string;
|
||||
fileSha1?: string;
|
||||
fileDuration: number;
|
||||
msgInfo: NapProtoEncodeStructType<typeof MsgInfo> | null = null;
|
||||
|
||||
constructor(element: SendPttElement) {
|
||||
super(element);
|
||||
this.filePath = element.pttElement.filePath;
|
||||
this.fileSize = +element.pttElement.fileSize; // TODO: cc
|
||||
this.fileMd5 = element.pttElement.md5HexStr;
|
||||
this.fileDuration = Math.round(element.pttElement.duration); // TODO: cc
|
||||
}
|
||||
|
||||
get valid(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
buildElement(): NapProtoEncodeStructType<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}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import * as zlib from "node:zlib";
|
||||
import * as crypto from "node:crypto";
|
||||
import { calculateSha1 } from "@/core/packet/utils/crypto/hash";
|
||||
import { computeMd5AndLengthWithLimit } from "@/core/packet/utils/crypto/hash";
|
||||
import { NapProtoMsg } from "@/core/packet/proto/NapProto";
|
||||
import { OidbSvcTrpcTcpBase } from "@/core/packet/proto/oidb/OidbBase";
|
||||
import { OidbSvcTrpcTcp0X9067_202 } from "@/core/packet/proto/oidb/Oidb.0x9067_202";
|
||||
@ -11,16 +11,29 @@ import { NTV2RichMediaReq } from "@/core/packet/proto/oidb/common/Ntv2.RichMedia
|
||||
import { HttpConn0x6ff_501 } from "@/core/packet/proto/action/action";
|
||||
import { LongMsgResult, SendLongMsgReq } from "@/core/packet/proto/message/action";
|
||||
import { PacketMsgBuilder } from "@/core/packet/msg/builder";
|
||||
import { PacketMsgPicElement } from "@/core/packet/msg/element";
|
||||
import {
|
||||
PacketMsgFileElement,
|
||||
PacketMsgPicElement,
|
||||
PacketMsgPttElement,
|
||||
PacketMsgVideoElement
|
||||
} from "@/core/packet/msg/element";
|
||||
import { LogWrapper } from "@/common/log";
|
||||
import { PacketMsg } from "@/core/packet/msg/message";
|
||||
import { OidbSvcTrpcTcp0x6D6 } from "@/core/packet/proto/oidb/Oidb.0x6D6";
|
||||
import { OidbSvcTrpcTcp0XE37_1200 } from "@/core/packet/proto/oidb/Oidb.0xE37_1200";
|
||||
import { PacketMsgConverter } from "@/core/packet/msg/converter";
|
||||
import { PacketClient } from "@/core/packet/client";
|
||||
import { OidbSvcTrpcTcp0XE37_1700 } from "@/core/packet/proto/oidb/Oidb.0xE37_1700";
|
||||
import { OidbSvcTrpcTcp0XE37_800 } from "@/core/packet/proto/oidb/Oidb.0XE37_800";
|
||||
import { OidbSvcTrpcTcp0XEB7 } from "./proto/oidb/Oidb.0xEB7";
|
||||
|
||||
export type PacketHexStr = string & { readonly hexNya: unique symbol };
|
||||
|
||||
export interface OidbPacket {
|
||||
cmd: string;
|
||||
data: PacketHexStr
|
||||
}
|
||||
|
||||
export class PacketPacker {
|
||||
readonly logger: LogWrapper;
|
||||
readonly client: PacketClient;
|
||||
@ -34,30 +47,34 @@ export class PacketPacker {
|
||||
this.packetConverter = new PacketMsgConverter(logger);
|
||||
}
|
||||
|
||||
private toHexStr(byteArray: Uint8Array): PacketHexStr {
|
||||
private packetPacket(byteArray: Uint8Array): PacketHexStr {
|
||||
return Buffer.from(byteArray).toString('hex') as PacketHexStr;
|
||||
}
|
||||
|
||||
packOidbPacket(cmd: number, subCmd: number, body: Uint8Array, isUid: boolean = true, isLafter: boolean = false): Uint8Array {
|
||||
return new NapProtoMsg(OidbSvcTrpcTcpBase).encode({
|
||||
packOidbPacket(cmd: number, subCmd: number, body: Uint8Array, isUid: boolean = true, isLafter: boolean = false): OidbPacket {
|
||||
const data = new NapProtoMsg(OidbSvcTrpcTcpBase).encode({
|
||||
command: cmd,
|
||||
subCommand: subCmd,
|
||||
body: body,
|
||||
isReserved: isUid ? 1 : 0
|
||||
});
|
||||
return {
|
||||
cmd: `OidbSvcTrpcTcp.0x${cmd.toString(16).toUpperCase()}_${subCmd}`,
|
||||
data: this.packetPacket(data)
|
||||
}
|
||||
}
|
||||
|
||||
packPokePacket(peer: number, group?: number): PacketHexStr {
|
||||
packPokePacket(peer: number, group?: number): OidbPacket {
|
||||
const oidb_0xed3 = new NapProtoMsg(OidbSvcTrpcTcp0XED3_1).encode({
|
||||
uin: peer,
|
||||
groupUin: group,
|
||||
friendUin: group ?? peer,
|
||||
ext: 0
|
||||
});
|
||||
return this.toHexStr(this.packOidbPacket(0xed3, 1, oidb_0xed3));
|
||||
return this.packOidbPacket(0xed3, 1, oidb_0xed3);
|
||||
}
|
||||
|
||||
packRkeyPacket(): PacketHexStr {
|
||||
packRkeyPacket(): OidbPacket {
|
||||
const oidb_0x9067_202 = new NapProtoMsg(OidbSvcTrpcTcp0X9067_202).encode({
|
||||
reqHead: {
|
||||
common: {
|
||||
@ -77,10 +94,10 @@ export class PacketPacker {
|
||||
key: [10, 20, 2]
|
||||
},
|
||||
});
|
||||
return this.toHexStr(this.packOidbPacket(0x9067, 202, oidb_0x9067_202));
|
||||
return this.packOidbPacket(0x9067, 202, oidb_0x9067_202);
|
||||
}
|
||||
|
||||
packSetSpecialTittlePacket(groupCode: string, uid: string, tittle: string): PacketHexStr {
|
||||
packSetSpecialTittlePacket(groupCode: string, uid: string, tittle: string): OidbPacket {
|
||||
const oidb_0x8FC_2_body = new NapProtoMsg(OidbSvcTrpcTcp0X8FC_2_Body).encode({
|
||||
targetUid: uid,
|
||||
specialTitle: tittle,
|
||||
@ -91,15 +108,15 @@ export class PacketPacker {
|
||||
groupUin: +groupCode,
|
||||
body: oidb_0x8FC_2_body
|
||||
});
|
||||
return this.toHexStr(this.packOidbPacket(0x8FC, 2, oidb_0x8FC_2, false, false));
|
||||
return this.packOidbPacket(0x8FC, 2, oidb_0x8FC_2, false, false);
|
||||
}
|
||||
|
||||
packStatusPacket(uin: number): PacketHexStr {
|
||||
packStatusPacket(uin: number): OidbPacket {
|
||||
const oidb_0xfe1_2 = new NapProtoMsg(OidbSvcTrpcTcp0XFE1_2).encode({
|
||||
uin: uin,
|
||||
key: [{ key: 27372 }]
|
||||
key: [{key: 27372}]
|
||||
});
|
||||
return this.toHexStr(this.packOidbPacket(0xfe1, 2, oidb_0xfe1_2));
|
||||
return this.packOidbPacket(0xfe1, 2, oidb_0xfe1_2);
|
||||
}
|
||||
|
||||
async packUploadForwardMsg(selfUid: string, msg: PacketMsg[], groupUin: number = 0): Promise<PacketHexStr> {
|
||||
@ -131,12 +148,12 @@ export class PacketPacker {
|
||||
}
|
||||
);
|
||||
// this.logger.logDebug("packUploadForwardMsg REQ!!!", req);
|
||||
return this.toHexStr(req);
|
||||
return this.packetPacket(req);
|
||||
}
|
||||
|
||||
// highway part
|
||||
packHttp0x6ff_501(): PacketHexStr {
|
||||
return this.toHexStr(new NapProtoMsg(HttpConn0x6ff_501).encode({
|
||||
return this.packetPacket(new NapProtoMsg(HttpConn0x6ff_501).encode({
|
||||
httpConn: {
|
||||
field1: 0,
|
||||
field2: 0,
|
||||
@ -153,7 +170,7 @@ export class PacketPacker {
|
||||
}));
|
||||
}
|
||||
|
||||
async packUploadGroupImgReq(groupUin: number, img: PacketMsgPicElement): Promise<PacketHexStr> {
|
||||
async packUploadGroupImgReq(groupUin: number, img: PacketMsgPicElement): Promise<OidbPacket> {
|
||||
const req = new NapProtoMsg(NTV2RichMediaReq).encode(
|
||||
{
|
||||
reqHead: {
|
||||
@ -177,9 +194,9 @@ export class PacketPacker {
|
||||
uploadInfo: [
|
||||
{
|
||||
fileInfo: {
|
||||
fileSize: Number(img.size),
|
||||
fileSize: +img.size,
|
||||
fileHash: img.md5,
|
||||
fileSha1: this.toHexStr(await calculateSha1(img.path)),
|
||||
fileSha1: img.sha1!,
|
||||
fileName: img.name,
|
||||
type: {
|
||||
type: 1,
|
||||
@ -218,10 +235,10 @@ 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: {
|
||||
@ -245,9 +262,9 @@ export class PacketPacker {
|
||||
uploadInfo: [
|
||||
{
|
||||
fileInfo: {
|
||||
fileSize: Number(img.size),
|
||||
fileSize: +img.size,
|
||||
fileHash: img.md5,
|
||||
fileSha1: this.toHexStr(await calculateSha1(img.path)),
|
||||
fileSha1: img.sha1!,
|
||||
fileName: img.name,
|
||||
type: {
|
||||
type: 1,
|
||||
@ -286,24 +303,379 @@ export class PacketPacker {
|
||||
}
|
||||
}
|
||||
);
|
||||
return this.toHexStr(this.packOidbPacket(0x11c5, 100, req, true, false));
|
||||
return this.packOidbPacket(0x11c5, 100, req, true, false);
|
||||
}
|
||||
|
||||
packGroupFileDownloadReq(groupUin: number, fileUUID: string): PacketHexStr {
|
||||
return this.toHexStr(
|
||||
this.packOidbPacket(0x6D6, 2, new NapProtoMsg(OidbSvcTrpcTcp0x6D6).encode({
|
||||
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({
|
||||
reqHead: {
|
||||
common: {
|
||||
requestId: 3,
|
||||
command: 100
|
||||
},
|
||||
scene: {
|
||||
requestType: 2,
|
||||
businessType: 2,
|
||||
sceneType: 2,
|
||||
group: {
|
||||
groupUin: groupUin
|
||||
},
|
||||
},
|
||||
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(0x11EA, 100, req, true, false);
|
||||
}
|
||||
|
||||
async packUploadC2CVideoReq(peerUin: string, 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({
|
||||
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: {
|
||||
groupUin: groupUin,
|
||||
appId: 7,
|
||||
busId: 102,
|
||||
fileId: fileUUID
|
||||
}
|
||||
}), true, false)
|
||||
}), true, false
|
||||
);
|
||||
}
|
||||
|
||||
packC2CFileDownloadReq(selfUid: string, fileUUID: string, fileHash: string): PacketHexStr {
|
||||
return this.toHexStr(
|
||||
return this.packetPacket(
|
||||
new NapProtoMsg(OidbSvcTrpcTcp0XE37_1200).encode({
|
||||
subCommand: 1200,
|
||||
field2: 1,
|
||||
@ -321,4 +693,16 @@ export class PacketPacker {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
packGroupSignReq(uin: string, groupCode: string): OidbPacket {
|
||||
return this.packOidbPacket(0XEB7, 1, new NapProtoMsg(OidbSvcTrpcTcp0XEB7).encode(
|
||||
{
|
||||
body: {
|
||||
uin: uin,
|
||||
groupUin: groupCode,
|
||||
version: "9.0.90"
|
||||
}
|
||||
}
|
||||
), false, false);
|
||||
}
|
||||
}
|
||||
|
@ -113,8 +113,26 @@ export const Permission = {
|
||||
|
||||
export const FileExtra = {
|
||||
file: ProtoField(1, () => NotOnlineFile),
|
||||
field6: ProtoField(6, () => PrivateFileExtra),
|
||||
};
|
||||
|
||||
export const PrivateFileExtra = {
|
||||
field2: ProtoField(2, () => PrivateFileExtraField2),
|
||||
}
|
||||
|
||||
export const PrivateFileExtraField2 = {
|
||||
field1: ProtoField(1, ScalarType.UINT32),
|
||||
fileUuid: ProtoField(4, ScalarType.STRING),
|
||||
fileName: ProtoField(5, ScalarType.STRING),
|
||||
field6: ProtoField(6, ScalarType.UINT32),
|
||||
field7: ProtoField(7, ScalarType.BYTES),
|
||||
field8: ProtoField(8, ScalarType.BYTES),
|
||||
timestamp1: ProtoField(9, ScalarType.UINT32),
|
||||
fileHash: ProtoField(14, ScalarType.STRING),
|
||||
selfUid: ProtoField(15, ScalarType.STRING),
|
||||
destUid: ProtoField(16, ScalarType.STRING),
|
||||
}
|
||||
|
||||
export const GroupFileExtra = {
|
||||
field1: ProtoField(1, ScalarType.UINT32),
|
||||
fileName: ProtoField(2, ScalarType.STRING),
|
||||
@ -132,8 +150,9 @@ export const GroupFileExtraInfo = {
|
||||
fileSize: ProtoField(3, ScalarType.UINT64),
|
||||
fileName: ProtoField(4, ScalarType.STRING),
|
||||
field5: ProtoField(5, ScalarType.UINT32),
|
||||
field7: ProtoField(7, ScalarType.STRING),
|
||||
fileMd5: ProtoField(8, ScalarType.STRING),
|
||||
fileSha: ProtoField(6, ScalarType.BYTES),
|
||||
extInfoString: ProtoField(7, ScalarType.STRING),
|
||||
fileMd5: ProtoField(8, ScalarType.BYTES),
|
||||
};
|
||||
|
||||
export const ImageExtraUrl = {
|
||||
|
62
src/core/packet/proto/oidb/Oidb.0XE37_800.ts
Normal file
62
src/core/packet/proto/oidb/Oidb.0XE37_800.ts
Normal 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),
|
||||
}
|
@ -30,7 +30,7 @@ export const OidbSvcTrpcTcp0XE37_1200ResponseBody = {
|
||||
field10: ProtoField(10, ScalarType.UINT32, true),
|
||||
state: ProtoField(20, ScalarType.STRING, true),
|
||||
result: ProtoField(30, () => OidbSvcTrpcTcp0XE37_1200Result, true),
|
||||
metadata: ProtoField(40, () => OidbSvcTrpcTcp0XE37_1200Metadata, true),
|
||||
metadata: ProtoField(40, () => OidbSvcTrpcTcp0XE37_800_1200Metadata, true),
|
||||
};
|
||||
|
||||
export const OidbSvcTrpcTcp0XE37_1200Result = {
|
||||
@ -43,7 +43,7 @@ export const OidbSvcTrpcTcp0XE37_1200Result = {
|
||||
extra: ProtoField(120, ScalarType.BYTES, true),
|
||||
};
|
||||
|
||||
export const OidbSvcTrpcTcp0XE37_1200Metadata = {
|
||||
export const OidbSvcTrpcTcp0XE37_800_1200Metadata = {
|
||||
uin: ProtoField(1, ScalarType.UINT32, true),
|
||||
field2: ProtoField(2, ScalarType.UINT32, true),
|
||||
field3: ProtoField(3, ScalarType.UINT32, true),
|
||||
|
23
src/core/packet/proto/oidb/Oidb.0xE37_1700.ts
Normal file
23
src/core/packet/proto/oidb/Oidb.0xE37_1700.ts
Normal 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),
|
||||
}
|
12
src/core/packet/proto/oidb/Oidb.0xEB7.ts
Normal file
12
src/core/packet/proto/oidb/Oidb.0xEB7.ts
Normal 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),
|
||||
}
|
@ -2,6 +2,7 @@
|
||||
import * as crypto from 'crypto';
|
||||
import * as stream from 'stream';
|
||||
import * as fs from 'fs';
|
||||
import {CalculateStreamBytesTransform} from "@/core/packet/utils/crypto/sha1StreamBytesTransform";
|
||||
|
||||
function sha1Stream(readable: stream.Readable) {
|
||||
return new Promise((resolve, reject) => {
|
||||
@ -10,7 +11,37 @@ function sha1Stream(readable: stream.Readable) {
|
||||
}) as Promise<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> {
|
||||
const readable = fs.createReadStream(filePath);
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
19
src/core/packet/utils/crypto/sha1Stream.test.ts
Normal file
19
src/core/packet/utils/crypto/sha1Stream.test.ts
Normal 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();
|
118
src/core/packet/utils/crypto/sha1Stream.ts
Normal file
118
src/core/packet/utils/crypto/sha1Stream.ts
Normal 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;
|
||||
}
|
||||
}
|
53
src/core/packet/utils/crypto/sha1StreamBytesTransform.ts
Normal file
53
src/core/packet/utils/crypto/sha1StreamBytesTransform.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
22
src/onebot/action/extends/SetGroupSign.ts
Normal file
22
src/onebot/action/extends/SetGroupSign.ts
Normal 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);
|
||||
}
|
||||
}
|
@ -93,6 +93,7 @@ import { GetGroupFileUrl } from "@/onebot/action/file/GetGroupFileUrl";
|
||||
import { GetPacketStatus } from "@/onebot/action/packet/GetPacketStatus";
|
||||
import { FriendPoke } from "@/onebot/action/user/FriendPoke";
|
||||
import { GetCredentials } from './system/GetCredentials';
|
||||
import { SetGroupSign } from './extends/SetGroupSign';
|
||||
|
||||
|
||||
export type ActionMap = Map<string, BaseAction<any, any>>;
|
||||
@ -115,6 +116,7 @@ export function createActionMap(obContext: NapCatOneBot11Adapter, core: NapCatCo
|
||||
new SetQQAvatar(obContext, core),
|
||||
new TranslateEnWordToZn(obContext, core),
|
||||
new GetGroupRootFiles(obContext, core),
|
||||
new SetGroupSign(obContext, core),
|
||||
// onebot11
|
||||
new SendLike(obContext, core),
|
||||
new GetMsg(obContext, core),
|
||||
|
@ -117,7 +117,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
|
||||
if (getSpecialMsgNum(payload, OB11MessageDataType.node)) {
|
||||
const packetMode = this.core.apis.PacketApi.available;
|
||||
const returnMsgAndResId = packetMode
|
||||
? await this.handleForwardedNodesPacket(peer, messages as OB11MessageNode[])
|
||||
? await this.handleForwardedNodesPacket(peer, messages as OB11MessageNode[], payload.source, payload.news, payload.summary, payload.prompt)
|
||||
: await this.handleForwardedNodes(peer, messages as OB11MessageNode[]);
|
||||
if (returnMsgAndResId.message) {
|
||||
const msgShortId = MessageUnique.createUniqueMsgId({
|
||||
@ -146,7 +146,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
|
||||
}
|
||||
|
||||
// TODO: recursively handle forwarded nodes
|
||||
private async handleForwardedNodesPacket(msgPeer: Peer, messageNodes: OB11MessageNode[]): Promise<{
|
||||
private async handleForwardedNodesPacket(msgPeer: Peer, messageNodes: OB11MessageNode[], source?: string, news?: { text: string }[], summary?: string, prompt?: string): Promise<{
|
||||
message: RawMessage | null,
|
||||
res_id?: string
|
||||
}> {
|
||||
@ -157,7 +157,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
|
||||
const OB11Data = normalize(node.data.content);
|
||||
const { sendElements } = await this.obContext.apis.MsgApi.createSendElements(OB11Data, msgPeer);
|
||||
const packetMsgElements: rawMsgWithSendMsg = {
|
||||
senderUin: node.data.user_id ?? +this.core.selfInfo.uin,
|
||||
senderUin: Number(node.data.user_id) ?? +this.core.selfInfo.uin,
|
||||
senderName: node.data.nickname,
|
||||
groupId: msgPeer.chatType === ChatType.KCHATTYPEGROUP ? +msgPeer.peerUid : undefined,
|
||||
time: Date.now(),
|
||||
@ -172,7 +172,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
|
||||
}
|
||||
}
|
||||
const resid = await this.core.apis.PacketApi.sendUploadForwardMsg(packetMsg, msgPeer.chatType === ChatType.KCHATTYPEGROUP ? +msgPeer.peerUid : 0);
|
||||
const forwardJson = ForwardMsgBuilder.fromPacketMsg(resid, packetMsg);
|
||||
const forwardJson = ForwardMsgBuilder.fromPacketMsg(resid, packetMsg, source, news, summary, prompt);
|
||||
const finallySendElements = {
|
||||
elementType: ElementType.ARK,
|
||||
elementId: "",
|
||||
|
@ -134,5 +134,7 @@ export enum ActionName {
|
||||
GetGuildProfile = 'get_guild_service_profile',
|
||||
|
||||
GetGroupIgnoredNotifies = 'get_group_ignored_notifies',
|
||||
|
||||
SetGroupSign = "set_group_sign",
|
||||
// UploadForwardMsg = "upload_forward_msg",
|
||||
}
|
||||
|
@ -16,8 +16,8 @@ export interface OB11Message {
|
||||
message_id: number,
|
||||
message_seq: number, // 和message_id一样
|
||||
real_id: number,
|
||||
user_id: number,
|
||||
group_id?: number,
|
||||
user_id: number | string, // number
|
||||
group_id?: number | string, // number
|
||||
message_type: 'private' | 'group',
|
||||
sub_type?: 'friend' | 'group' | 'normal',
|
||||
sender: OB11Sender,
|
||||
@ -149,7 +149,7 @@ export interface OB11MessageNode {
|
||||
type: OB11MessageDataType.node;
|
||||
data: {
|
||||
id?: string
|
||||
user_id?: number
|
||||
user_id?: number | string // number
|
||||
nickname: string
|
||||
content: OB11MessageMixType
|
||||
};
|
||||
@ -221,6 +221,10 @@ export interface OB11PostSendMsg {
|
||||
message: OB11MessageMixType;
|
||||
messages?: OB11MessageMixType; // 兼容 go-cqhttp
|
||||
auto_escape?: boolean | string
|
||||
source?: string,
|
||||
news?: { text: string }[],
|
||||
summary?: string,
|
||||
prompt?: string
|
||||
}
|
||||
export interface OB11PostContext {
|
||||
message_type?: 'private' | 'group'
|
||||
|
@ -30,7 +30,7 @@ async function onSettingWindowCreated(view: Element) {
|
||||
SettingItem(
|
||||
'<span id="napcat-update-title">Napcat</span>',
|
||||
undefined,
|
||||
SettingButton('V3.1.3', 'napcat-update-button', 'secondary'),
|
||||
SettingButton('V3.1.5', 'napcat-update-button', 'secondary'),
|
||||
),
|
||||
]),
|
||||
SettingList([
|
||||
|
@ -164,7 +164,7 @@ async function onSettingWindowCreated(view) {
|
||||
SettingItem(
|
||||
'<span id="napcat-update-title">Napcat</span>',
|
||||
void 0,
|
||||
SettingButton("V3.1.3", "napcat-update-button", "secondary")
|
||||
SettingButton("V3.1.5", "napcat-update-button", "secondary")
|
||||
)
|
||||
]),
|
||||
SettingList([
|
||||
|
Loading…
x
Reference in New Issue
Block a user