429 lines
18 KiB
TypeScript
429 lines
18 KiB
TypeScript
import * as crypto from 'crypto';
|
|
import { PacketContext } from '@/napcat-core/packet/context/packetContext';
|
|
import * as trans from '@/napcat-core/packet/transformer';
|
|
import { PacketMsg } from '@/napcat-core/packet/message/message';
|
|
import {
|
|
PacketMsgFileElement,
|
|
PacketMsgPicElement,
|
|
PacketMsgPttElement,
|
|
PacketMsgReplyElement,
|
|
PacketMsgVideoElement,
|
|
} from '@/napcat-core/packet/message/element';
|
|
import { ChatType, MsgSourceType, NTMsgType, RawMessage } from '@/napcat-core/index';
|
|
import { MiniAppRawData, MiniAppReqParams } from '@/napcat-core/packet/entities/miniApp';
|
|
import { AIVoiceChatType } from '@/napcat-core/packet/entities/aiChat';
|
|
import { NapProtoDecodeStructType, NapProtoEncodeStructType, NapProtoMsg } from 'napcat-protobuf';
|
|
import { IndexNode, LongMsgResult, MsgInfo, PushMsgBody } from '@/napcat-core/packet/transformer/proto';
|
|
import { OidbPacket } from '@/napcat-core/packet/transformer/base';
|
|
import { ImageOcrResult } from '@/napcat-core/packet/entities/ocrResult';
|
|
import { gunzipSync } from 'zlib';
|
|
import { PacketMsgConverter } from '@/napcat-core/packet/message/converter';
|
|
import { UploadForwardMsgParams } from '@/napcat-core/packet/transformer/message/UploadForwardMsgV2';
|
|
import { assertUploadResults } from '@/napcat-core/packet/context/uploadResult';
|
|
|
|
export class PacketOperationContext {
|
|
private readonly context: PacketContext;
|
|
|
|
constructor (context: PacketContext) {
|
|
this.context = context;
|
|
}
|
|
|
|
async sendPacket<T extends boolean = false> (pkt: OidbPacket, rsp?: T): Promise<T extends true ? Buffer : void> {
|
|
return await this.context.client.sendOidbPacket(pkt, rsp);
|
|
}
|
|
|
|
async SendPoke (is_group: boolean, peer: number, target?: number) {
|
|
const req = trans.SendPoke.build(is_group, peer, target ?? peer);
|
|
await this.context.client.sendOidbPacket(req);
|
|
}
|
|
|
|
async SetGroupTodo (groupUin: number, msgSeq: string) {
|
|
const req = trans.SetGroupTodo.build(groupUin, msgSeq);
|
|
await this.context.client.sendOidbPacket(req, true);
|
|
}
|
|
|
|
async CompleteGroupTodo (groupUin: number, msgSeq: string) {
|
|
const req = trans.CompleteGroupTodo.build(groupUin, msgSeq);
|
|
await this.context.client.sendOidbPacket(req, true);
|
|
}
|
|
|
|
async CancelGroupTodo (groupUin: number, msgSeq: string) {
|
|
const req = trans.CancelGroupTodo.build(groupUin, msgSeq);
|
|
await this.context.client.sendOidbPacket(req, true);
|
|
}
|
|
|
|
async FetchRkey (timeout: number = 10000) {
|
|
const req = trans.FetchRkey.build();
|
|
const resp = await this.context.client.sendOidbPacket(req, true, timeout);
|
|
const res = trans.FetchRkey.parse(resp);
|
|
return res.data.rkeyList;
|
|
}
|
|
|
|
async GroupSign (groupUin: number) {
|
|
const req = trans.GroupSign.build(this.context.napcore.basicInfo.uin, groupUin);
|
|
await this.context.client.sendOidbPacket(req);
|
|
}
|
|
|
|
async GetStrangerStatus (uin: number) {
|
|
let status = 0;
|
|
try {
|
|
const req = trans.GetStrangerInfo.build(uin);
|
|
const resp = await this.context.client.sendOidbPacket(req, true);
|
|
const res = trans.GetStrangerInfo.parse(resp);
|
|
const extBigInt = BigInt(res.data.status.value);
|
|
if (extBigInt <= 10n) {
|
|
return { status: Number(extBigInt) * 10, ext_status: 0 };
|
|
}
|
|
status = Number((extBigInt & 0xff00n) + ((extBigInt >> 16n) & 0xffn));
|
|
return { status: 10, ext_status: status };
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
async SetGroupSpecialTitle (groupUin: number, uid: string, title: string) {
|
|
const req = trans.SetSpecialTitle.build(groupUin, uid, title);
|
|
await this.context.client.sendOidbPacket(req);
|
|
}
|
|
|
|
async UploadResources (msg: PacketMsg[], groupUin: number = 0) {
|
|
const chatType = groupUin ? ChatType.KCHATTYPEGROUP : ChatType.KCHATTYPEC2C;
|
|
const peerUid = groupUin ? String(groupUin) : this.context.napcore.basicInfo.uid;
|
|
const reqList = msg.flatMap((m) =>
|
|
m.msg
|
|
.map((e) => {
|
|
if (e instanceof PacketMsgPicElement) {
|
|
return this.context.highway.uploadImage({ chatType, peerUid }, e);
|
|
} else if (e instanceof PacketMsgVideoElement) {
|
|
return this.context.highway.uploadVideo({ chatType, peerUid }, e);
|
|
} else if (e instanceof PacketMsgPttElement) {
|
|
return this.context.highway.uploadPtt({ chatType, peerUid }, e);
|
|
} else if (e instanceof PacketMsgFileElement) {
|
|
return this.context.highway.uploadFile({ chatType, peerUid }, e);
|
|
}
|
|
return null;
|
|
})
|
|
.filter(Boolean)
|
|
);
|
|
const res = await Promise.allSettled(reqList);
|
|
assertUploadResults(res, this.context.logger);
|
|
}
|
|
|
|
async UploadImage (img: PacketMsgPicElement) {
|
|
await this.context.highway.uploadImage(
|
|
{
|
|
chatType: ChatType.KCHATTYPEC2C,
|
|
peerUid: this.context.napcore.basicInfo.uid,
|
|
},
|
|
img
|
|
);
|
|
const index = img.msgInfo?.msgInfoBody?.at(0)?.index;
|
|
if (!index) {
|
|
throw new Error('img.msgInfo?.msgInfoBody![0].index! is undefined');
|
|
}
|
|
return await this.GetImageUrl(this.context.napcore.basicInfo.uid, index);
|
|
}
|
|
|
|
async GetImageUrl (selfUid: string, node: NapProtoEncodeStructType<typeof IndexNode>) {
|
|
const req = trans.DownloadImage.build(selfUid, node);
|
|
const resp = await this.context.client.sendOidbPacket(req, true);
|
|
const res = trans.DownloadImage.parse(resp);
|
|
return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`;
|
|
}
|
|
|
|
async GetPttUrl (selfUid: string, node: NapProtoEncodeStructType<typeof IndexNode>, timeout?: number) {
|
|
const req = trans.DownloadPtt.build(selfUid, node);
|
|
const resp = await this.context.client.sendOidbPacket(req, true, timeout);
|
|
const res = trans.DownloadPtt.parse(resp);
|
|
return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`;
|
|
}
|
|
|
|
async GetVideoUrl (selfUid: string, node: NapProtoEncodeStructType<typeof IndexNode>, timeout?: number) {
|
|
const req = trans.DownloadVideo.build(selfUid, node);
|
|
const resp = await this.context.client.sendOidbPacket(req, true, timeout);
|
|
const res = trans.DownloadVideo.parse(resp);
|
|
return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`;
|
|
}
|
|
|
|
async GetGroupImageUrl (groupUin: number, node: NapProtoEncodeStructType<typeof IndexNode>, timeout?: number) {
|
|
const req = trans.DownloadGroupImage.build(groupUin, node);
|
|
const resp = await this.context.client.sendOidbPacket(req, true, timeout);
|
|
const res = trans.DownloadImage.parse(resp);
|
|
return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`;
|
|
}
|
|
|
|
async GetGroupPttUrl (groupUin: number, node: NapProtoEncodeStructType<typeof IndexNode>, timeout?: number) {
|
|
const req = trans.DownloadGroupPtt.build(groupUin, node);
|
|
const resp = await this.context.client.sendOidbPacket(req, true, timeout);
|
|
const res = trans.DownloadImage.parse(resp);
|
|
return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`;
|
|
}
|
|
|
|
async GetGroupVideoUrl (groupUin: number, node: NapProtoEncodeStructType<typeof IndexNode>, timeout: number = 20000) {
|
|
const req = trans.DownloadGroupVideo.build(groupUin, node);
|
|
const resp = await this.context.client.sendOidbPacket(req, true, timeout);
|
|
const res = trans.DownloadImage.parse(resp);
|
|
return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`;
|
|
}
|
|
|
|
async ImageOCR (imgUrl: string) {
|
|
const req = trans.ImageOCR.build(imgUrl);
|
|
const resp = await this.context.client.sendOidbPacket(req, true);
|
|
const res = trans.ImageOCR.parse(resp);
|
|
return {
|
|
texts: res.ocrRspBody.textDetections.map((item) => {
|
|
return {
|
|
text: item.detectedText,
|
|
confidence: item.confidence,
|
|
coordinates: item.polygon.coordinates.map((c) => {
|
|
return {
|
|
x: c.x,
|
|
y: c.y,
|
|
};
|
|
}),
|
|
};
|
|
}),
|
|
language: res.ocrRspBody.language,
|
|
} as ImageOcrResult;
|
|
}
|
|
|
|
private async SendPreprocess (msg: PacketMsg[], groupUin: number = 0) {
|
|
const ps = msg.map((m) => {
|
|
return m.msg.map(async (e) => {
|
|
if (e instanceof PacketMsgReplyElement && !e.targetElems) {
|
|
this.context.logger.debug('Cannot find reply element\'s targetElems, prepare to fetch it...');
|
|
if (!e.targetPeer?.peerUid) {
|
|
this.context.logger.error('targetPeer is undefined!');
|
|
}
|
|
let targetMsg: NapProtoEncodeStructType<typeof PushMsgBody>[] | undefined;
|
|
if (e.isGroupReply) {
|
|
targetMsg = await this.FetchGroupMessage(+(e.targetPeer?.peerUid ?? 0), e.targetMessageSeq, e.targetMessageSeq);
|
|
} else {
|
|
targetMsg = await this.FetchC2CMessage(await this.context.napcore.basicInfo.uin2uid(e.targetUin), e.targetMessageSeq, e.targetMessageSeq);
|
|
}
|
|
e.targetElems = targetMsg.at(0)?.body?.richText?.elems;
|
|
e.targetSourceMsg = targetMsg.at(0);
|
|
}
|
|
});
|
|
}).flat();
|
|
await Promise.all(ps);
|
|
await this.UploadResources(msg, groupUin);
|
|
}
|
|
|
|
async FetchGroupMessage (groupUin: number, startSeq: number, endSeq: number): Promise<NapProtoDecodeStructType<typeof PushMsgBody>[]> {
|
|
const req = trans.FetchGroupMessage.build(groupUin, startSeq, endSeq);
|
|
const resp = await this.context.client.sendOidbPacket(req, true);
|
|
const res = trans.FetchGroupMessage.parse(resp);
|
|
return res.body.messages;
|
|
}
|
|
|
|
async FetchC2CMessage (targetUid: string, startSeq: number, endSeq: number): Promise<NapProtoDecodeStructType<typeof PushMsgBody>[]> {
|
|
const req = trans.FetchC2CMessage.build(targetUid, startSeq, endSeq);
|
|
const resp = await this.context.client.sendOidbPacket(req, true);
|
|
const res = trans.FetchC2CMessage.parse(resp);
|
|
return res.messages;
|
|
}
|
|
|
|
async UploadForwardMsg (msg: PacketMsg[], groupUin: number = 0) {
|
|
await this.SendPreprocess(msg, groupUin);
|
|
const req = trans.UploadForwardMsg.build(this.context.napcore.basicInfo.uid, msg, groupUin);
|
|
const resp = await this.context.client.sendOidbPacket(req, true);
|
|
const res = trans.UploadForwardMsg.parse(resp);
|
|
return res.result.resId;
|
|
}
|
|
|
|
async UploadForwardMsgV2 (msg: UploadForwardMsgParams[], groupUin: number = 0) {
|
|
// 遍历上传资源(跳过已有原始数据的项)
|
|
await Promise.all(msg.filter(item => item.actionMsg).map(async (item) => {
|
|
await this.SendPreprocess(item.actionMsg!, groupUin);
|
|
}));
|
|
const req = trans.UploadForwardMsgV2.build(this.context.napcore.basicInfo.uid, msg, groupUin);
|
|
const resp = await this.context.client.sendOidbPacket(req, true);
|
|
const res = trans.UploadForwardMsg.parse(resp);
|
|
return res.result.resId;
|
|
}
|
|
|
|
async MoveGroupFile (
|
|
groupUin: number,
|
|
fileUUID: string,
|
|
currentParentDirectory: string,
|
|
targetParentDirectory: string
|
|
) {
|
|
const req = trans.MoveGroupFile.build(groupUin, fileUUID, currentParentDirectory, targetParentDirectory);
|
|
const resp = await this.context.client.sendOidbPacket(req, true);
|
|
const res = trans.MoveGroupFile.parse(resp);
|
|
return res.move.retCode;
|
|
}
|
|
|
|
async RenameGroupFile (groupUin: number, fileUUID: string, currentParentDirectory: string, newName: string) {
|
|
const req = trans.RenameGroupFile.build(groupUin, fileUUID, currentParentDirectory, newName);
|
|
const resp = await this.context.client.sendOidbPacket(req, true);
|
|
const res = trans.RenameGroupFile.parse(resp);
|
|
return res.rename.retCode;
|
|
}
|
|
|
|
async GetGroupFileUrl (groupUin: number, fileUUID: string, timeout?: number) {
|
|
const req = trans.DownloadGroupFile.build(groupUin, fileUUID);
|
|
const resp = await this.context.client.sendOidbPacket(req, true, timeout);
|
|
const res = trans.DownloadGroupFile.parse(resp);
|
|
return `https://${res.download.downloadDns}/ftn_handler/${Buffer.from(res.download.downloadUrl).toString('hex')}/?fname=`;
|
|
}
|
|
|
|
async GetPrivateFileUrl (self_id: string, fileUUID: string, md5: string, timeout?: number) {
|
|
const req = trans.DownloadPrivateFile.build(self_id, fileUUID, md5);
|
|
const resp = await this.context.client.sendOidbPacket(req, true, timeout);
|
|
const res = trans.DownloadPrivateFile.parse(resp);
|
|
return `http://${res.body?.result?.server}:${res.body?.result?.port}${res.body?.result?.url?.slice(8)}&isthumb=0`;
|
|
}
|
|
|
|
async GetMiniAppAdaptShareInfo (param: MiniAppReqParams) {
|
|
const req = trans.GetMiniAppAdaptShareInfo.build(param);
|
|
const resp = await this.context.client.sendOidbPacket(req, true);
|
|
const res = trans.GetMiniAppAdaptShareInfo.parse(resp);
|
|
return JSON.parse(res.content.jsonContent) as MiniAppRawData;
|
|
}
|
|
|
|
async FetchAiVoiceList (groupUin: number, chatType: AIVoiceChatType) {
|
|
const req = trans.FetchAiVoiceList.build(groupUin, chatType);
|
|
const resp = await this.context.client.sendOidbPacket(req, true);
|
|
const res = trans.FetchAiVoiceList.parse(resp);
|
|
if (!res.content) return null;
|
|
return res.content.map((item) => {
|
|
return {
|
|
category: item.category,
|
|
voices: item.voices,
|
|
};
|
|
});
|
|
}
|
|
|
|
async GetAiVoice (
|
|
groupUin: number,
|
|
voiceId: string,
|
|
text: string,
|
|
chatType: AIVoiceChatType
|
|
): Promise<NapProtoDecodeStructType<typeof MsgInfo>> {
|
|
let reqTime = 0;
|
|
const reqMaxTime = 30;
|
|
const sessionId = crypto.randomBytes(4).readUInt32BE(0);
|
|
while (true) {
|
|
if (reqTime >= reqMaxTime) {
|
|
throw new Error(`sendAiVoiceChatReq failed after ${reqMaxTime} times`);
|
|
}
|
|
reqTime++;
|
|
const req = trans.GetAiVoice.build(groupUin, voiceId, text, sessionId, chatType);
|
|
const resp = await this.context.client.sendOidbPacket(req, true);
|
|
const res = trans.GetAiVoice.parse(resp);
|
|
if (!res.msgInfo) continue;
|
|
return res.msgInfo;
|
|
}
|
|
}
|
|
|
|
async FetchForwardMsgRaw (resId: string) {
|
|
const req = trans.DownloadForwardMsg.build(this.context.napcore.basicInfo.uid, resId);
|
|
const resp = await this.context.client.sendOidbPacket(req, true);
|
|
const res = trans.DownloadForwardMsg.parse(resp);
|
|
const inflate = gunzipSync(res.result.payload);
|
|
const result = new NapProtoMsg(LongMsgResult).decode(inflate);
|
|
return result.action;
|
|
}
|
|
|
|
async FetchForwardMsg (res_id: string): Promise<RawMessage[]> {
|
|
const req = trans.DownloadForwardMsg.build(this.context.napcore.basicInfo.uid, res_id);
|
|
const resp = await this.context.client.sendOidbPacket(req, true);
|
|
const res = trans.DownloadForwardMsg.parse(resp);
|
|
const inflate = gunzipSync(res.result.payload);
|
|
const result = new NapProtoMsg(LongMsgResult).decode(inflate);
|
|
|
|
const main = result.action.find((r) => r.actionCommand === 'MultiMsg');
|
|
if (!main?.actionData.msgBody) {
|
|
throw new Error('msgBody is empty');
|
|
}
|
|
this.context.logger.debug('rawChains ', inflate.toString('hex'));
|
|
|
|
const messagesPromises = main.actionData.msgBody.map(async (msg) => {
|
|
if (!msg?.body?.richText?.elems) {
|
|
throw new Error('msg.body.richText.elems is empty');
|
|
}
|
|
const rawChains = new PacketMsgConverter().packetMsgToRaw(msg?.body?.richText?.elems);
|
|
const elements = await Promise.all(
|
|
rawChains.map(async ([element, rawElem]) => {
|
|
if (element.picElement && rawElem?.commonElem?.pbElem) {
|
|
const extra = new NapProtoMsg(MsgInfo).decode(rawElem.commonElem.pbElem);
|
|
const index = extra?.msgInfoBody[0]?.index;
|
|
if (msg?.responseHead.grp !== undefined) {
|
|
const groupUin = msg?.responseHead.grp?.groupUin ?? 0;
|
|
element.picElement = {
|
|
...element.picElement,
|
|
originImageUrl: await this.GetGroupImageUrl(groupUin, index!),
|
|
};
|
|
} else {
|
|
element.picElement = {
|
|
...element.picElement,
|
|
originImageUrl: await this.GetImageUrl(this.context.napcore.basicInfo.uid, index!),
|
|
};
|
|
}
|
|
return element;
|
|
}
|
|
if (element.videoElement && rawElem?.commonElem?.pbElem) {
|
|
const extra = new NapProtoMsg(MsgInfo).decode(rawElem.commonElem.pbElem);
|
|
const index = extra?.msgInfoBody[0]?.index;
|
|
if (!index) return element;
|
|
if (msg?.responseHead.grp !== undefined) {
|
|
element.videoElement = {
|
|
...element.videoElement,
|
|
filePath: await this.GetGroupVideoUrl(msg.responseHead.grp.groupUin ?? 0, index),
|
|
};
|
|
} else {
|
|
element.videoElement = {
|
|
...element.videoElement,
|
|
filePath: await this.GetVideoUrl(this.context.napcore.basicInfo.uid, index),
|
|
};
|
|
}
|
|
return element;
|
|
}
|
|
return element;
|
|
})
|
|
);
|
|
const isGroup = msg?.responseHead.grp !== undefined;
|
|
const peerUin = isGroup
|
|
? String(msg?.responseHead.grp?.groupUin ?? 0)
|
|
: String(msg?.responseHead.fromUin ?? 0);
|
|
const peerUid = isGroup
|
|
? String(msg?.responseHead.grp?.groupUin ?? 0)
|
|
: String(msg?.responseHead.fromUid ?? '');
|
|
const sendNickName = isGroup
|
|
? (msg?.responseHead.grp?.memberName ?? '')
|
|
: (msg?.responseHead.forward?.friendName ?? '');
|
|
|
|
return {
|
|
chatType: isGroup ? ChatType.KCHATTYPEGROUP : ChatType.KCHATTYPEC2C,
|
|
elements,
|
|
guildId: '',
|
|
isOnlineMsg: false,
|
|
msgId: '7467703692092974645', // TODO: no necessary
|
|
msgRandom: '0',
|
|
msgSeq: String(msg.contentHead.sequence ?? 0),
|
|
msgTime: String(msg.contentHead.timeStamp ?? 0),
|
|
msgType: NTMsgType.KMSGTYPEMIX,
|
|
parentMsgIdList: [],
|
|
parentMsgPeer: {
|
|
chatType: isGroup ? ChatType.KCHATTYPEGROUP : ChatType.KCHATTYPEC2C,
|
|
peerUid,
|
|
},
|
|
peerName: '',
|
|
peerUid,
|
|
peerUin,
|
|
recallTime: '0',
|
|
records: [],
|
|
sendNickName,
|
|
sendRemarkName: sendNickName,
|
|
senderUid: msg?.responseHead.fromUid ?? '',
|
|
senderUin: String(msg?.responseHead.fromUin ?? 0),
|
|
sourceType: MsgSourceType.K_DOWN_SOURCETYPE_UNKNOWN,
|
|
subMsgType: 1,
|
|
};
|
|
});
|
|
return await Promise.all(messagesPromises);
|
|
}
|
|
}
|