NapCatQQ source snapshot
This commit is contained in:
commit
f954252a34
1111 files changed
+139867
No files matched your search
@@ -0,0 +1,83 @@
|
||||
import { NativePacketClient } from '@/napcat-core/packet/client/nativeClient';
|
||||
import { OidbPacket } from '@/napcat-core/packet/transformer/base';
|
||||
import { PacketLogger } from '@/napcat-core/packet/context/loggerContext';
|
||||
import { NapCoreContext } from '@/napcat-core/packet/context/napCoreContext';
|
||||
import { Napi2NativeLoader } from '@/napcat-core/packet/handler/napi2nativeLoader';
|
||||
|
||||
export class LogStack {
|
||||
private stack: string[] = [];
|
||||
private readonly logger: PacketLogger;
|
||||
|
||||
constructor (logger: PacketLogger) {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
push (msg: string) {
|
||||
this.stack.push(msg);
|
||||
}
|
||||
|
||||
pushLogInfo (msg: string) {
|
||||
this.logger.info(msg);
|
||||
this.stack.push(`${new Date().toISOString()} [INFO] ${msg}`);
|
||||
}
|
||||
|
||||
pushLogWarn (msg: string) {
|
||||
this.logger.warn(msg);
|
||||
this.stack.push(`${new Date().toISOString()} [WARN] ${msg}`);
|
||||
}
|
||||
|
||||
pushLogError (msg: string) {
|
||||
this.logger.error(msg);
|
||||
this.stack.push(`${new Date().toISOString()} [ERROR] ${msg}`);
|
||||
}
|
||||
|
||||
clear () {
|
||||
this.stack = [];
|
||||
}
|
||||
|
||||
content () {
|
||||
return this.stack.join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
export class PacketClientContext {
|
||||
private readonly napCore: NapCoreContext;
|
||||
private readonly logger: PacketLogger;
|
||||
private readonly logStack: LogStack;
|
||||
private readonly napi2nativeLoader: Napi2NativeLoader;
|
||||
private readonly _client: NativePacketClient;
|
||||
|
||||
constructor (napCore: NapCoreContext, logger: PacketLogger, napi2nativeLoader: Napi2NativeLoader) {
|
||||
this.napCore = napCore;
|
||||
this.logger = logger;
|
||||
this.logStack = new LogStack(logger);
|
||||
this.napi2nativeLoader = napi2nativeLoader;
|
||||
this._client = this.newClient();
|
||||
}
|
||||
|
||||
get available (): boolean {
|
||||
return this._client.available;
|
||||
}
|
||||
|
||||
get clientLogStack (): string {
|
||||
return this._client.logStack.content();
|
||||
}
|
||||
|
||||
async init (pid: number, recv: string, send: string): Promise<void> {
|
||||
await this._client.init(pid, recv, send);
|
||||
}
|
||||
|
||||
async sendOidbPacket<T extends boolean = false> (pkt: OidbPacket, rsp?: T, timeout?: number): Promise<T extends true ? Buffer : void> {
|
||||
const raw = await this._client.sendOidbPacket(pkt, rsp, timeout);
|
||||
return raw.data as T extends true ? Buffer : void;
|
||||
}
|
||||
|
||||
private newClient (): NativePacketClient {
|
||||
this.logger.info('使用 NativePacketClient 作为后端');
|
||||
const client = new NativePacketClient(this.napCore, this.logger, this.logStack, this.napi2nativeLoader);
|
||||
if (!client.check()) {
|
||||
throw new Error('[Core] [Packet] NativePacketClient 不可用,NapCat.Packet将不会加载!');
|
||||
}
|
||||
return client;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { NapCoreContext } from '@/napcat-core/packet/context/napCoreContext';
|
||||
import { LogWrapper, LogLevel } from '@/napcat-core/helper/log';
|
||||
|
||||
// TODO: check bind?
|
||||
export class PacketLogger {
|
||||
private readonly napLogger: LogWrapper;
|
||||
|
||||
constructor (napcore: NapCoreContext) {
|
||||
this.napLogger = napcore.logger;
|
||||
}
|
||||
|
||||
private _log (level: LogLevel, ...msg: any[]): void {
|
||||
this.napLogger._log(level, '[Core] [Packet] ' + msg);
|
||||
}
|
||||
|
||||
debug (...msg: any[]): void {
|
||||
this._log(LogLevel.DEBUG, msg);
|
||||
}
|
||||
|
||||
info (...msg: any[]): void {
|
||||
this._log(LogLevel.INFO, msg);
|
||||
}
|
||||
|
||||
warn (...msg: any[]): void {
|
||||
this._log(LogLevel.WARN, msg);
|
||||
}
|
||||
|
||||
error (...msg: any[]): void {
|
||||
this._log(LogLevel.ERROR, msg);
|
||||
}
|
||||
|
||||
fatal (...msg: any[]): void {
|
||||
this._log(LogLevel.FATAL, msg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { NapCatCore } from '@/napcat-core/index';
|
||||
|
||||
export interface NapCoreCompatBasicInfo {
|
||||
readonly requireMinNTQQBuild: (buildVer: string) => boolean;
|
||||
readonly uin: number;
|
||||
readonly uid: string;
|
||||
readonly uin2uid: (uin: number) => Promise<string>;
|
||||
readonly uid2uin: (uid: string) => Promise<number>;
|
||||
readonly sendSsoCmdReqByContend: (cmd: string, trace_id: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export class NapCoreContext {
|
||||
private readonly core: NapCatCore;
|
||||
|
||||
constructor (core: NapCatCore) {
|
||||
this.core = core;
|
||||
}
|
||||
|
||||
get logger () {
|
||||
return this.core.context.logger;
|
||||
}
|
||||
|
||||
get basicInfo () {
|
||||
return {
|
||||
requireMinNTQQBuild: (buildVer: string) => this.core.context.basicInfoWrapper.requireMinNTQQBuild(buildVer),
|
||||
uin: +this.core.selfInfo.uin,
|
||||
uid: this.core.selfInfo.uid,
|
||||
uin2uid: (uin: number) => this.core.apis.UserApi.getUidByUinV2(String(uin)).then(res => res ?? ''),
|
||||
uid2uin: (uid: string) => this.core.apis.UserApi.getUinByUidV2(uid).then(res => +res),
|
||||
} as NapCoreCompatBasicInfo;
|
||||
}
|
||||
|
||||
get config () {
|
||||
return this.core.configLoader.configData;
|
||||
}
|
||||
|
||||
get napi2nativeLoader () {
|
||||
return this.core.context.napi2nativeLoader;
|
||||
}
|
||||
|
||||
sendSsoCmdReqByContend = (cmd: string, data: Buffer) => this.core.context.session.getMsgService().sendSsoCmdReqByContend(cmd, data);
|
||||
}
|
||||
@@ -0,0 +1,428 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { PacketHighwayContext } from '@/napcat-core/packet/highway/highwayContext';
|
||||
import { NapCatCore } from '@/napcat-core/index';
|
||||
import { PacketLogger } from '@/napcat-core/packet/context/loggerContext';
|
||||
import { NapCoreContext } from '@/napcat-core/packet/context/napCoreContext';
|
||||
import { PacketClientContext } from '@/napcat-core/packet/context/clientContext';
|
||||
import { PacketOperationContext } from '@/napcat-core/packet/context/operationContext';
|
||||
import { PacketMsgConverter } from '@/napcat-core/packet/message/converter';
|
||||
|
||||
export class PacketContext {
|
||||
readonly msgConverter: PacketMsgConverter;
|
||||
readonly napcore: NapCoreContext;
|
||||
readonly logger: PacketLogger;
|
||||
readonly client: PacketClientContext;
|
||||
readonly highway: PacketHighwayContext;
|
||||
readonly operation: PacketOperationContext;
|
||||
|
||||
constructor (core: NapCatCore) {
|
||||
this.msgConverter = new PacketMsgConverter();
|
||||
this.napcore = new NapCoreContext(core);
|
||||
this.logger = new PacketLogger(this.napcore);
|
||||
this.client = new PacketClientContext(this.napcore, this.logger, this.napcore.napi2nativeLoader);
|
||||
this.highway = new PacketHighwayContext(this.napcore, this.logger, this.client);
|
||||
this.operation = new PacketOperationContext(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
export interface UploadResultLogger {
|
||||
warn (message: string): unknown;
|
||||
error (message: string): unknown;
|
||||
}
|
||||
|
||||
export function assertUploadResults (
|
||||
results: PromiseSettledResult<unknown>[],
|
||||
logger: UploadResultLogger
|
||||
): void {
|
||||
const failures = results.filter((result): result is PromiseRejectedResult => result.status === 'rejected');
|
||||
if (failures.length === 0) return;
|
||||
|
||||
logger.warn(`上传资源${results.length}个,失败${failures.length}个`);
|
||||
results.forEach((result, index) => {
|
||||
if (result.status === 'rejected') {
|
||||
const reason = result.reason instanceof Error
|
||||
? result.reason.stack ?? result.reason.message
|
||||
: String(result.reason);
|
||||
logger.error(`上传第${index + 1}个资源失败:${reason}`);
|
||||
}
|
||||
});
|
||||
throw new AggregateError(
|
||||
failures.map(result => result.reason),
|
||||
`上传转发消息资源失败:${failures.length}/${results.length}`
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user