diff --git a/external/LiteLoaderWrapper.zip b/external/LiteLoaderWrapper.zip index 44b654a5..a6edb42f 100644 Binary files a/external/LiteLoaderWrapper.zip and b/external/LiteLoaderWrapper.zip differ diff --git a/launcher/qqnt.json b/launcher/qqnt.json index 41140f9b..45b53805 100644 --- a/launcher/qqnt.json +++ b/launcher/qqnt.json @@ -1,9 +1,9 @@ { "name": "qq-chat", - "version": "9.9.15-28418", - "verHash": "206bfa62", - "linuxVersion": "3.2.12-28418", - "linuxVerHash": "0256c948", + "version": "9.9.15-28788", + "verHash": "73b0c8f6", + "linuxVersion": "3.2.12-28788", + "linuxVerHash": "55fb6434", "type": "module", "private": true, "description": "QQ", @@ -18,7 +18,7 @@ "qd": "externals/devtools/cli/index.js" }, "main": "./loadNapCat.js", - "buildVersion": "28418", + "buildVersion": "28788", "isPureShell": true, "isByteCodeShell": true, "platform": "win32", diff --git a/manifest.json b/manifest.json index 360bb5e7..ebe6edfd 100644 --- a/manifest.json +++ b/manifest.json @@ -4,7 +4,7 @@ "name": "NapCatQQ", "slug": "NapCat.Framework", "description": "高性能的 OneBot 11 协议实现", - "version": "2.6.27", + "version": "3.0.0", "icon": "./logo.png", "authors": [ { diff --git a/package.json b/package.json index fc907f48..769f865f 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "napcat", "private": true, "type": "module", - "version": "2.6.27", + "version": "3.0.0", "scripts": { "build:framework": "vite build --mode framework", "build:shell": "vite build --mode shell", diff --git a/src/common/helper.ts b/src/common/helper.ts index 934a95d9..30d97267 100644 --- a/src/common/helper.ts +++ b/src/common/helper.ts @@ -25,8 +25,8 @@ export async function solveAsyncProblem Promise { } while (this.keyToValue.size > this.maxSize || this.valueToKey.size > this.maxSize) { const oldestKey = this.keyToValue.keys().next().value; + // @ts-ignore this.valueToKey.delete(this.keyToValue.get(oldestKey)!); + // @ts-ignore this.keyToValue.delete(oldestKey); } } diff --git a/src/common/version.ts b/src/common/version.ts index 63a0246c..235e557a 100644 --- a/src/common/version.ts +++ b/src/common/version.ts @@ -1 +1 @@ -export const napCatVersion = '2.6.27'; +export const napCatVersion = '3.0.0'; diff --git a/src/core/apis/file.ts b/src/core/apis/file.ts index 7d7c6356..3ce58117 100644 --- a/src/core/apis/file.ts +++ b/src/core/apis/file.ts @@ -30,6 +30,7 @@ export class NTQQFileApi { context: InstanceContext; core: NapCatCore; rkeyManager: RkeyManager; + packetRkey: Array<{ rkey: string; time: number; type: number; }> | undefined; constructor(context: InstanceContext, core: NapCatCore) { this.context = context; @@ -370,29 +371,43 @@ export class NTQQFileApi { const isNTV2 = imageAppid && ['1406', '1407'].includes(imageAppid); const imageFileId = parsedUrl.searchParams.get('fileid'); - let rkeyData = { + const rkeyData = { private_rkey: 'CAQSKAB6JWENi5LM_xp9vumLbuThJSaYf-yzMrbZsuq7Uz2qEc3Rbib9LP4', group_rkey: 'CAQSKAB6JWENi5LM_xp9vumLbuThJSaYf-yzMrbZsuq7Uz2qffcqm614gds', online_rkey: false }; - try { - let tempRkeyData = await this.rkeyManager.getRkey(); - rkeyData.group_rkey = tempRkeyData.group_rkey; - rkeyData.private_rkey = tempRkeyData.private_rkey; - rkeyData.online_rkey = tempRkeyData.expired_time > Date.now() / 1000; - } catch (e) { - this.context.logger.logError.bind(this.context.logger)('获取rkey失败 Fallback Old Mode', e); + if (this.core.apis.PacketApi.available) { + if ((!this.packetRkey || this.packetRkey[0].time > Date.now() / 1000)) { + this.packetRkey = await this.core.apis.PacketApi.sendRkeyPacket(); + } + if (this.packetRkey.length > 0) { + rkeyData.group_rkey = this.packetRkey[1].rkey.slice(6); + rkeyData.private_rkey = this.packetRkey[0].rkey.slice(6); + rkeyData.online_rkey = true; + } + } + } catch (error: any) { + this.context.logger.logError.bind(this.context.logger)('获取rkey失败', error.message); } - + if (!rkeyData.online_rkey) { + try { + const tempRkeyData = await this.rkeyManager.getRkey(); + rkeyData.group_rkey = tempRkeyData.group_rkey; + rkeyData.private_rkey = tempRkeyData.private_rkey; + rkeyData.online_rkey = tempRkeyData.expired_time > Date.now() / 1000; + } catch (e) { + this.context.logger.logError.bind(this.context.logger)('获取rkey失败 Fallback Old Mode', e); + } + } if (isNTV2 && urlRkey) { return IMAGE_HTTP_HOST_NT + urlRkey; } else if (isNTV2 && rkeyData.online_rkey) { - let rkey = imageAppid === '1406' ? rkeyData.private_rkey : rkeyData.group_rkey; + const rkey = imageAppid === '1406' ? rkeyData.private_rkey : rkeyData.group_rkey; return IMAGE_HTTP_HOST_NT + url + `&rkey=${rkey}`; } else if (isNTV2 && imageFileId) { - let rkey = imageAppid === '1406' ? rkeyData.private_rkey : rkeyData.group_rkey; + const rkey = imageAppid === '1406' ? rkeyData.private_rkey : rkeyData.group_rkey; return IMAGE_HTTP_HOST + `/download?appid=${imageAppid}&fileid=${imageFileId}&rkey=${rkey}`; } diff --git a/src/core/apis/friend.ts b/src/core/apis/friend.ts index 66733cbc..4c0d387e 100644 --- a/src/core/apis/friend.ts +++ b/src/core/apis/friend.ts @@ -10,7 +10,9 @@ export class NTQQFriendApi { this.context = context; this.core = core; } - + async setBuddyRemark(uid: string, remark: string) { + return this.context.session.getBuddyService().setBuddyRemark(uid, remark); + } async getBuddyV2SimpleInfoMap(refresh = false) { const buddyService = this.context.session.getBuddyService(); const buddyListV2 = refresh ? await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL) : await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL); diff --git a/src/core/apis/group.ts b/src/core/apis/group.ts index 307ab3ab..c907ca3b 100644 --- a/src/core/apis/group.ts +++ b/src/core/apis/group.ts @@ -9,22 +9,10 @@ import { MemberExtSourceType, NapCatCore, } from '@/core'; -import { isNumeric, sleep, solveAsyncProblem } from '@/common/helper'; +import { isNumeric, solveAsyncProblem } from '@/common/helper'; import { LimitedHashTable } from '@/common/message-unique'; import { NTEventWrapper } from '@/common/event'; -import { encodeGroupPoke } from '../proto/Poke'; -import { randomUUID } from 'crypto'; -import { RequestUtil } from '@/common/request'; -interface recvPacket -{ - type: string,//仅recv - trace_id_md5?: string, - data: { - seq: number, - hex_data: string, - cmd: string - } -} + export class NTQQGroupApi { context: InstanceContext; core: NapCatCore; @@ -46,12 +34,9 @@ export class NTQQGroupApi { this.groupCache.set(group.groupCode, group); } this.context.logger.logDebug(`加载${this.groups.length}个群组缓存完成`); - //console.log('pid', process.pid); - // this.session = await frida.attach(process.pid); - // setTimeout(async () => { - // this.sendPocketRkey(); - // }, 10000); + // process.pid 调试点 } + async getCoreAndBaseInfo(uids: string[]) { return await this.core.eventWrapper.callNoListenerEvent( 'NodeIKernelProfileService/getCoreAndBaseInfo', @@ -59,17 +44,7 @@ export class NTQQGroupApi { uids, ); } - async sendPocketRkey() { - let hex = '08E7A00210CA01221D0A130A05080110CA011206A80602B006011A0208022206080A081408022A006001'; - let ret = await this.core.apis.PacketApi.sendPacket('OidbSvcTrpcTcp.0x9067_202', hex, true); - //console.log('ret: ', ret); - } - async sendPacketPoke(group: number, peer: number) { - let data = encodeGroupPoke(group, peer); - let hex = Buffer.from(data).toString('hex'); - let retdata = await this.core.apis.PacketApi.sendPacket('OidbSvcTrpcTcp.0xed3_1', hex, false); - //console.log('sendPacketPoke', retdata); - } + async fetchGroupEssenceList(groupCode: string) { const pskey = (await this.core.apis.UserApi.getPSkey(['qun.qq.com'])).domainPskeyMap.get('qun.qq.com')!; return this.context.session.getGroupService().fetchGroupEssenceList({ @@ -78,7 +53,9 @@ export class NTQQGroupApi { pageLimit: 300, }, pskey); } - + async getGroupShutUpMemberList(groupCode: string) { + return this.context.session.getGroupService().getGroupShutUpMemberList(groupCode); + } async clearGroupNotifiesUnreadCount(uk: boolean) { return this.context.session.getGroupService().clearGroupNotifiesUnreadCount(uk); } @@ -166,7 +143,7 @@ export class NTQQGroupApi { let members = this.groupMemberCache.get(groupCodeStr); if (!members) { try { - members = await this.getGroupMembersV2(groupCodeStr); + members = await this.getGroupMembers(groupCodeStr); // 更新群成员列表 this.groupMemberCache.set(groupCodeStr, members); } catch (e) { @@ -187,11 +164,12 @@ export class NTQQGroupApi { let member = getMember(); if (!member) { - members = await this.getGroupMembersV2(groupCodeStr); + members = await this.getGroupMembers(groupCodeStr); member = getMember(); } return member; } + async getGroupRecommendContactArkJson(groupCode: string) { return this.context.session.getGroupService().getGroupRecommendContactArkJson(groupCode); } @@ -297,6 +275,7 @@ export class NTQQGroupApi { } return member; } + async searchGroup(groupCode: string) { const [, ret] = await this.core.eventWrapper.callNormalEventV2( 'NodeIKernelSearchService/searchGroup', @@ -314,6 +293,7 @@ export class NTQQGroupApi { ); return ret.groupInfos.find(g => g.groupCode === groupCode); } + async getGroupMemberEx(GroupCode: string, uid: string, forced = false, retry = 2) { const data = await solveAsyncProblem((eventWrapper: NTEventWrapper, GroupCode: string, uid: string, forced = false) => { return eventWrapper.callNormalEventV2( @@ -335,36 +315,19 @@ export class NTQQGroupApi { } return undefined; } + async getGroupMembersV2(groupQQ: string, num = 3000): Promise> { - const groupService = this.context.session.getGroupService(); - const sceneId = groupService.createMemberListScene(groupQQ, 'groupMemberList_MainWindow'); - const listener = this.core.eventWrapper.registerListen( - 'NodeIKernelGroupListener/onMemberListChange', - 1, - 5000, - (params) => params.sceneId === sceneId, - ); - try { - const [membersFromFunc, membersFromListener] = await Promise.allSettled([ - groupService.getNextMemberList(sceneId, undefined, num), - listener, - ]); - if (membersFromFunc.status === 'fulfilled' && membersFromListener.status === 'fulfilled') { - return new Map([ - ...membersFromFunc.value.result.infos, - ...membersFromListener.value[0].infos, - ]); - } - if (membersFromFunc.status === 'fulfilled') { - return membersFromFunc.value.result.infos; - } - if (membersFromListener.status === 'fulfilled') { - return membersFromListener.value[0].infos; - } - throw new Error('获取群成员列表失败'); - } finally { - groupService.destroyMemberListScene(sceneId); + const sceneId = this.context.session.getGroupService().createMemberListScene(groupQQ, 'groupMemberList_MainWindow'); + let once = this.core.eventWrapper.registerListen('NodeIKernelGroupListener/onMemberListChange', 1, 2000, (params) => params.sceneId === sceneId) + .catch(); + const result = await this.context.session.getGroupService().getNextMemberList(sceneId!, undefined, num); + if (result.errCode !== 0) { + throw new Error('获取群成员列表出错,' + result.errMsg); } + if (result.result.infos.size === 0) { + return (await once)[0].infos; + } + return result.result.infos; } async getGroupMembers(groupQQ: string, num = 3000): Promise> { diff --git a/src/core/apis/packet.ts b/src/core/apis/packet.ts index 6666b871..dfae3401 100644 --- a/src/core/apis/packet.ts +++ b/src/core/apis/packet.ts @@ -1,8 +1,18 @@ -import { InstanceContext, NapCatCore } from '..'; -import { RequestUtil } from '@/common/request'; +import * as os from 'os'; +import {ChatType, InstanceContext, NapCatCore} from '..'; import offset from '@/core/external/offset.json'; -import * as crypto from 'crypto'; -import { PacketClient } from '../helper/packet'; +import {PacketClient, RecvPacketData} from '@/core/packet/client'; +import {PacketSession} from "@/core/packet/session"; +import {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'; +import {OidbSvcTrpcTcp0XFE1_2RSP} from '@/core/packet/proto/oidb/Oidb.0XFE1_2'; +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"; interface OffsetType { [key: string]: { @@ -12,57 +22,122 @@ interface OffsetType { } const typedOffset: OffsetType = offset; + export class NTQQPacketApi { context: InstanceContext; core: NapCatCore; + logger: LogWrapper serverUrl: string | undefined; - qqversion: string | undefined; - isInit: boolean = false; - PacketClient: PacketClient | undefined; + qqVersion: string | undefined; + packetSession: PacketSession | undefined; + constructor(context: InstanceContext, core: NapCatCore) { this.context = context; this.core = core; - let config = this.core.configLoader.configData; + this.logger = core.context.logger; + this.packetSession = undefined; + const config = this.core.configLoader.configData; if (config && config.packetServer && config.packetServer.length > 0) { - let serverurl = this.core.configLoader.configData.packetServer ?? '127.0.0.1:8086'; - this.InitSendPacket(serverurl, this.context.basicInfoWrapper.getFullQQVesion()) + const serverUrl = this.core.configLoader.configData.packetServer ?? '127.0.0.1:8086'; + this.InitSendPacket(serverUrl, this.context.basicInfoWrapper.getFullQQVesion()) .then() .catch(this.core.context.logger.logError.bind(this.core.context.logger)); + } else { + this.core.context.logger.logWarn('PacketServer is not set, will not init NapCat.Packet!'); } } + + get available(): boolean { + return this.packetSession?.client.available ?? false; + } + async InitSendPacket(serverUrl: string, qqversion: string) { this.serverUrl = serverUrl; - this.qqversion = qqversion; - let offsetTable: OffsetType = offset; - if (!offsetTable[qqversion]) return false; - let url = 'ws://' + this.serverUrl + '/ws'; - this.PacketClient = new PacketClient(url, this.core.context.logger); - await this.PacketClient.connect(); - await this.PacketClient.init(process.pid, offsetTable[qqversion].recv, offsetTable[qqversion].send); - this.isInit = true; - return this.isInit; + this.qqVersion = qqversion; + const offsetTable: OffsetType = offset; + const table = offsetTable[qqversion + '-' + os.arch()]; + if (!table) return false; + const url = 'ws://' + this.serverUrl + '/ws'; + this.packetSession = new PacketSession(this.core.context.logger, new PacketClient(url, this.core)); + await this.packetSession.client.connect(); + await this.packetSession.client.init(process.pid, table.recv, table.send); + return true; } - randText(len: number) { - let text = ''; - let possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - for (let i = 0; i < len; i++) { - text += possible.charAt(Math.floor(Math.random() * possible.length)); - } - return text; + + async sendPacket(cmd: string, data: PacketHexStr, rsp = false): Promise { + return this.packetSession!.client.sendPacket(cmd, data, rsp); } - async sendPacket(cmd: string, data: string, rsp = false) { - // wtfk tx - // 校验失败和异常 可能返回undefined - return new Promise((resolve, reject) => { - if (!this.isInit || !this.PacketClient?.isConnected) { - this.core.context.logger.logError('PacketClient is not init'); - return undefined; + + async sendPokePacket(group: number, peer: number) { + const data = this.packetSession?.packer.packPokePacket(group, peer); + await this.sendPacket('OidbSvcTrpcTcp.0xed3_1', data!, false); + } + + async sendRkeyPacket() { + const packet = this.packetSession?.packer.packRkeyPacket(); + const ret = await this.sendPacket('OidbSvcTrpcTcp.0x9067_202', 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 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 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 + const extBigInt = BigInt(ext); // 转换为 BigInt + if (extBigInt <= 10n) { + return { status: Number(extBigInt) * 10, ext_status: 0 }; } - let md5 = crypto.createHash('md5').update(data).digest('hex'); - let trace_id = (this.randText(4) + md5 + data).slice(0, data.length / 2); - this.PacketClient?.sendCommand(cmd, data, trace_id, rsp, 5000, async () => { - await this.core.context.session.getMsgService().sendSsoCmdReqByContend(cmd, trace_id); - }).then((res) => resolve(res)).catch((e) => reject(e)); - }); + status = Number((extBigInt & 0xff00n) + ((extBigInt >> 16n) & 0xffn)); // 使用 BigInt 操作符 + return { status: 10, ext_status: status }; + } catch (error) { + return undefined; + } } -} \ No newline at end of file + + 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); + } + + private 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 + }, e)); + } + } + } + return Promise.all(reqList); + } + + async sendUploadForwardMsg(msg: PacketMsg[], groupUin: number = 0) { + await this.uploadResources(msg, groupUin); + const data = await this.packetSession?.packer.packUploadForwardMsg(this.core.selfInfo.uid, msg, groupUin); + const ret = await this.sendPacket('trpc.group.long_msg_interface.MsgService.SsoSendLongMsg', data!, true); + this.logger.logDebug('sendUploadForwardMsg', ret); + const resp = new NapProtoMsg(SendLongMsgResp).decode(Buffer.from(ret.hex_data, 'hex')); + return resp.result.resId; + } + + 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 body = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(Buffer.from(ret.hex_data, 'hex')).body; + const resp = new NapProtoMsg(OidbSvcTrpcTcp0x6D6Response).decode(body); + if (resp.download.retCode !== 0){ + throw new Error(`sendGroupFileDownloadReq error: ${resp.download.clientWording}`); + } + return `https://${resp.download.downloadDns}/ftn_handler/${Buffer.from(resp.download.downloadUrl).toString('hex')}/?fname=` + } +} diff --git a/src/core/apis/user.ts b/src/core/apis/user.ts index f55b27c9..ed912396 100644 --- a/src/core/apis/user.ts +++ b/src/core/apis/user.ts @@ -68,8 +68,7 @@ export class NTQQUserApi { } async setQQAvatar(filePath: string) { - type setQQAvatarRet = { result: number, errMsg: string }; - const ret = await this.context.session.getProfileService().setHeader(filePath) as setQQAvatarRet; + const ret = await this.context.session.getProfileService().setHeader(filePath); return { result: ret?.result, errMsg: ret?.errMsg }; } @@ -124,10 +123,10 @@ export class NTQQUserApi { const ClientKeyData = await this.forceFetchClientKey(); const requestUrl = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + this.core.selfInfo.uin + '&clientkey=' + ClientKeyData.clientKey + '&u1=https%3A%2F%2F' + domain + '%2F' + this.core.selfInfo.uin + '%2Finfocenter&keyindex=19%27'; - let data = await RequestUtil.HttpsGetCookies(requestUrl); + const data = await RequestUtil.HttpsGetCookies(requestUrl); if (!data.p_skey || data.p_skey.length == 0) { try { - let pskey = (await this.getPSkey([domain])).domainPskeyMap.get(domain); + const pskey = (await this.getPSkey([domain])).domainPskeyMap.get(domain); if (pskey) data.p_skey = pskey; } catch { return data; diff --git a/src/core/entities/group.ts b/src/core/entities/group.ts index 25294de7..f486f9ec 100644 --- a/src/core/entities/group.ts +++ b/src/core/entities/group.ts @@ -117,6 +117,7 @@ export enum GroupMemberRole { } export interface GroupMember { + memberRealLevel: string | undefined; memberSpecialTitle?: string; avatarPath: string; cardName: string; diff --git a/src/core/entities/msg.ts b/src/core/entities/msg.ts index 03760e6f..d23d4a04 100644 --- a/src/core/entities/msg.ts +++ b/src/core/entities/msg.ts @@ -372,6 +372,7 @@ export interface ReplyElement { senderUin: string; senderUidStr?: string; replyMsgTime?: string; + replyMsgClientSeq?: string; } export interface SendReplyElement { @@ -391,7 +392,7 @@ export interface SendMarketFaceElement { marketFaceElement: MarketFaceElement; } -export interface SendstructLongMsgElement { +export interface SendStructLongMsgElement { elementType: ElementType.STRUCTLONGMSG; elementId: string; structLongMsgElement: StructLongMsgElement; diff --git a/src/core/entities/user.ts b/src/core/entities/user.ts index c71733b9..6730ab5a 100644 --- a/src/core/entities/user.ts +++ b/src/core/entities/user.ts @@ -288,9 +288,9 @@ export interface User { export interface SelfInfo extends User { online?: boolean; } +export type Friend = User; -export interface Friend extends User { -} +// 本来是 Friend extends User 现在用不到 export enum BizKey { KPRIVILEGEICON, diff --git a/src/core/external/appid.json b/src/core/external/appid.json index 84928941..2d2f05f9 100644 --- a/src/core/external/appid.json +++ b/src/core/external/appid.json @@ -19,19 +19,19 @@ "appid": 537246115, "qua": "V1_MAC_NQ_6.9.55_28131_GW_B" }, - "9.9.15-28327":{ + "9.9.15-28327": { "appid": 537249321, "qua": "V1_WIN_NQ_9.9.15_28327_GW_B" }, - "3.2.12-28327":{ + "3.2.12-28327": { "appid": 537249393, "qua": "V1_LNX_NQ_3.2.12_28327_GW_B" }, - "9.9.15-28418":{ + "9.9.15-28418": { "appid": 537249321, "qua": "V1_WIN_NQ_9.9.15_28418_GW_B" }, - "3.2.12-28418":{ + "3.2.12-28418": { "appid": 537249393, "qua": "V1_LNX_NQ_3.2.12_28418_GW_B" }, @@ -39,8 +39,16 @@ "appid": 537249367, "qua": "V1_MAC_NQ_6.9.56_28418_GW_B" }, - "9.9.15-28498":{ + "9.9.15-28498": { "appid": 537249321, "qua": "V1_WIN_NQ_9.9.15_28498_GW_B" + }, + "3.2.13-28788": { + "appid": 537249787, + "qua": "V1_LNX_NQ_3.2.13_28788_GW_B" + }, + "9.9.16-28788": { + "appid": 537249739, + "qua": "V1_WIN_NQ_9.9.16_28788_GW_B" } -} +} \ No newline at end of file diff --git a/src/core/external/offset.json b/src/core/external/offset.json index 2956b666..f18e11f3 100644 --- a/src/core/external/offset.json +++ b/src/core/external/offset.json @@ -1,14 +1,22 @@ { - "3.2.12-28418": { + "3.2.12-28418-x64": { "recv": "A0723E0", "send": "A06EAE0" }, - "9.9.15-28418": { + "9.9.15-28418-x64": { "recv": "37A9004", "send": "37A4BD0" }, - "9.9.15-28498": { + "9.9.15-28498-x64": { "recv": "37A9004", "send": "37A4BD0" + }, + "9.9.16-28788-x64": { + "send": "38076D0", + "recv": "380BB04" + }, + "3.2.13-28788-x64": { + "send": "A0CEC20", + "recv": "A0D2520" } } \ No newline at end of file diff --git a/src/core/helper/packet.ts b/src/core/helper/packet.ts deleted file mode 100644 index b287db42..00000000 --- a/src/core/helper/packet.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { LogWrapper } from "@/common/log"; -import { LRUCache } from "@/common/lru-cache"; -import WebSocket from "ws"; -import { createHash } from "crypto"; - -export class PacketClient { - private websocket: WebSocket | undefined; - public isConnected: boolean = false; - private reconnectAttempts: number = 0; - private maxReconnectAttempts: number = 5; - //trace_id-type callback - private cb = new LRUCache(500); - constructor(private url: string, public logger: LogWrapper) { } - - connect(): Promise { - return new Promise((resolve, reject) => { - this.logger.log.bind(this.logger)(`Attempting to connect to ${this.url}`); - this.websocket = new WebSocket(this.url); - this.websocket.on('error', (err) => this.logger.logError.bind(this.logger)('[Core] [Packet Server] Error:', err.message)); - - this.websocket.onopen = () => { - this.isConnected = true; - this.reconnectAttempts = 0; - this.logger.log.bind(this.logger)(`Connected to ${this.url}`); - resolve(); - }; - - this.websocket.onerror = (error) => { - this.logger.logError.bind(this.logger)(`WebSocket error: ${error}`); - reject(error); - }; - - this.websocket.onmessage = (event) => { - // const message = JSON.parse(event.data.toString()); - // console.log("Received message:", message); - this.handleMessage(event.data); - }; - - this.websocket.onclose = () => { - this.isConnected = false; - this.logger.logWarn.bind(this.logger)(`Disconnected from ${this.url}`); - this.attemptReconnect(); - }; - }); - } - - private attemptReconnect(): void { - try { - if (this.reconnectAttempts < this.maxReconnectAttempts) { - this.reconnectAttempts++; - this.logger.logError.bind(this.logger)(`Reconnecting attempt ${this.reconnectAttempts}`); - setTimeout(() => this.connect().then().catch(), 1000 * this.reconnectAttempts); - } else { - this.logger.logError.bind(this.logger)(`Max reconnect attempts reached. Could not reconnect to ${this.url}`); - } - } catch (error) { - this.logger.logError.bind(this.logger)(`Error attempting to reconnect: ${error}`); - } - } - async registerCallback(trace_id: string, type: string, callback: any): Promise { - this.cb.put(createHash('md5').update(trace_id).digest('hex') + type, callback); - } - - async init(pid: number, recv: string, send: string): Promise { - if (!this.isConnected || !this.websocket) { - throw new Error("WebSocket is not connected"); - } - - const initMessage = { - action: 'init', - pid: pid, - recv: recv, - send: send - }; - this.websocket.send(JSON.stringify(initMessage)); - } - - async sendCommand(cmd: string, data: string, trace_id: string, rsp: boolean = false, timeout: number = 5000, sendcb: any = () => { }): Promise { - return new Promise((resolve, reject) => { - if (!this.isConnected || !this.websocket) { - throw new Error("WebSocket is not connected"); - } - const commandMessage = { - action: 'send', - cmd: cmd, - data: data, - trace_id: trace_id - }; - this.websocket.send(JSON.stringify(commandMessage)); - if (rsp) { - this.registerCallback(trace_id, 'recv', (json: any) => { - clearTimeout(timeoutHandle); - resolve(json); - }); - } - this.registerCallback(trace_id, 'send', (json: any) => { - sendcb(json); - if (!rsp) { - clearTimeout(timeoutHandle); - resolve(json); - } - }); - const timeoutHandle = setTimeout(() => { - reject(new Error(`sendCommand timed out after ${timeout} ms`)); - }, timeout); - }); - } - private async handleMessage(message: any): Promise { - try { - - let json = JSON.parse(message.toString()); - let trace_id_md5 = json.trace_id_md5; - let action = json?.type ?? 'init'; - let event = this.cb.get(trace_id_md5 + action); - if (event) { - await event(json.data); - } - //console.log("Received message:", json); - } catch (error) { - this.logger.logError.bind(this.logger)(`Error parsing message: ${error}`); - } - } -} \ No newline at end of file diff --git a/src/core/helper/rkey.ts b/src/core/helper/rkey.ts index 852cafed..89e4229f 100644 --- a/src/core/helper/rkey.ts +++ b/src/core/helper/rkey.ts @@ -43,7 +43,7 @@ export class RkeyManager { //刷新rkey for (const url of this.serverUrl) { try { - let temp = await RequestUtil.HttpGetJson(url, 'GET'); + const temp = await RequestUtil.HttpGetJson(url, 'GET'); this.rkeyData = { group_rkey: temp.group_rkey.slice(6), private_rkey: temp.private_rkey.slice(6), diff --git a/src/core/packet/client.ts b/src/core/packet/client.ts new file mode 100644 index 00000000..55f74505 --- /dev/null +++ b/src/core/packet/client.ts @@ -0,0 +1,179 @@ +import { LogWrapper } from "@/common/log"; +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"; + +export interface RecvPacket { + type: string, // 仅recv + trace_id_md5?: string, + data: RecvPacketData +} + +export interface RecvPacketData { + seq: number + cmd: string + hex_data: string +} + +export class PacketClient { + private websocket: WebSocket | undefined; + private isConnected: boolean = false; + private reconnectAttempts: number = 0; + private readonly maxReconnectAttempts: number = 5;//现在暂时不可配置 + private readonly cb = new LRUCache Promise>(500); // trace_id-type callback + private readonly clientUrl: string = ''; + readonly napCatCore: NapCatCore; + private readonly logger: LogWrapper; + + constructor(url: string, core: NapCatCore) { + this.clientUrl = url; + this.napCatCore = core; + this.logger = core.context.logger; + } + + get available(): boolean { + return this.isConnected && this.websocket !== undefined; + } + + private randText(len: number) { + let text = ''; + const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + for (let i = 0; i < len; i++) { + text += possible.charAt(Math.floor(Math.random() * possible.length)); + } + return text; + } + + connect(): Promise { + return new Promise((resolve, reject) => { + //this.logger.log.bind(this.logger)(`[Core] [Packet Server] Attempting to connect to ${this.clientUrl}`); + this.websocket = new WebSocket(this.clientUrl); + this.websocket.on('error', (err) => {}/*this.logger.logError.bind(this.logger)('[Core] [Packet Server] Error:', err.message)*/); + + this.websocket.onopen = () => { + this.isConnected = true; + this.reconnectAttempts = 0; + this.logger.log.bind(this.logger)(`[Core] [Packet Server] Connected to ${this.clientUrl}`); + resolve(); + }; + + this.websocket.onerror = (error) => { + //this.logger.logError.bind(this.logger)(`WebSocket error: ${error}`); + reject(new Error(`${error.message}`)); + }; + + this.websocket.onmessage = (event) => { + // const message = JSON.parse(event.data.toString()); + // console.log("Received message:", message); + this.handleMessage(event.data).then().catch(); + }; + + this.websocket.onclose = () => { + this.isConnected = false; + //this.logger.logWarn.bind(this.logger)(`[Core] [Packet Server] Disconnected from ${this.clientUrl}`); + this.attemptReconnect(); + }; + }); + } + + private attemptReconnect(): void { + try { + if (this.reconnectAttempts < this.maxReconnectAttempts) { + this.reconnectAttempts++; + setTimeout(() => { + this.connect().catch((error) => { + this.logger.logError.bind(this.logger)(`[Core] [Packet Server] Reconnecting attempt failed,${error.message}`); + }); + }, 5000 * this.reconnectAttempts); + } else { + this.logger.logError.bind(this.logger)(`[Core] [Packet Server] Max reconnect attempts reached. ${this.clientUrl}`); + } + } catch (error: any) { + this.logger.logError.bind(this.logger)(`Error attempting to reconnect: ${error.message}`); + } + } + + private async registerCallback(trace_id: string, type: string, callback: (json: RecvPacketData) => Promise): Promise { + this.cb.put(createHash('md5').update(trace_id).digest('hex') + type, callback); + } + + async init(pid: number, recv: string, send: string): Promise { + if (!this.isConnected || !this.websocket) { + throw new Error("WebSocket is not connected"); + } + const initMessage = { + action: 'init', + pid: pid, + recv: recv, + send: send + }; + this.websocket.send(JSON.stringify(initMessage)); + } + + private async sendCommand(cmd: string, data: string, trace_id: string, rsp: boolean = false, timeout: number = 20000, sendcb: (json: RecvPacketData) => void = () => { + }): Promise { + return new Promise((resolve, reject) => { + if (!this.isConnected || !this.websocket) { + throw new Error("WebSocket is not connected"); + } + const commandMessage = { + action: 'send', + cmd: cmd, + data: data, + trace_id: trace_id + }; + this.websocket.send(JSON.stringify(commandMessage)); + if (rsp) { + this.registerCallback(trace_id, 'recv', async (json: RecvPacketData) => { + clearTimeout(timeoutHandle); + resolve(json); + }); + } + this.registerCallback(trace_id, 'send', async (json: RecvPacketData) => { + sendcb(json); + if (!rsp) { + clearTimeout(timeoutHandle); + resolve(json); + } + }); + const timeoutHandle = setTimeout(() => { + reject(new Error(`sendCommand timed out after ${timeout} ms for ${cmd} with trace_id ${trace_id}`)); + }, timeout); + }); + } + + private async handleMessage(message: Data): Promise { + try { + const json: RecvPacket = JSON.parse(message.toString()); + const trace_id_md5 = json.trace_id_md5; + const action = json?.type ?? 'init'; + const event = this.cb.get(trace_id_md5 + action); + if (event) { + await event(json.data); + } + //console.log("Received message:", json); + } catch (error) { + this.logger.logError.bind(this.logger)(`Error parsing message: ${error}`); + } + } + + async sendPacket(cmd: string, data: PacketHexStr, rsp = false): Promise { + // wtfk tx + // 校验失败和异常 可能返回undefined + return new Promise((resolve, reject) => { + if (!this.available) { + this.logger.logError('NapCat.Packet is not init'); + return undefined; + } + const md5 = crypto.createHash('md5').update(data).digest('hex'); + const trace_id = (this.randText(4) + md5 + data).slice(0, data.length / 2); + this.sendCommand(cmd, data, trace_id, rsp, 20000, async () => { + // await sleep(10); + await this.napCatCore.context.session.getMsgService().sendSsoCmdReqByContend(cmd, trace_id); + }).then((res) => resolve(res)).catch((e: Error) => reject(e)); + }); + } +} diff --git a/src/core/packet/highway/client.ts b/src/core/packet/highway/client.ts new file mode 100644 index 00000000..0583e676 --- /dev/null +++ b/src/core/packet/highway/client.ts @@ -0,0 +1,72 @@ +import * as stream from 'node:stream'; +import {ReadStream} from "node:fs"; +import {PacketHighwaySig} from "@/core/packet/highway/session"; +import {HighwayHttpUploader, HighwayTcpUploader} from "@/core/packet/highway/uploader"; +import {LogWrapper} from "@/common/log"; + +export interface PacketHighwayTrans { + uin: string; + cmd: number; + command: string; + data: stream.Readable; + sum: Uint8Array; + size: number; + ticket: Uint8Array; + loginSig?: Uint8Array; + ext: Uint8Array; + encrypt: boolean; + timeout?: number; + server: string; + port: number; +} + +export class PacketHighwayClient { + sig: PacketHighwaySig; + server: string = 'htdata3.qq.com'; + port: number = 80; + logger: LogWrapper; + + constructor(sig: PacketHighwaySig, logger: LogWrapper, server: string = 'htdata3.qq.com', port: number = 80) { + this.sig = sig; + this.logger = logger; + } + + changeServer(server: string, port: number) { + this.server = server; + this.port = port; + } + + private buildDataUpTrans(cmd: number, data: ReadStream, fileSize: number, md5: Uint8Array, extendInfo: Uint8Array, timeout: number = 3600): PacketHighwayTrans { + return { + uin: this.sig.uin, + cmd: cmd, + command: 'PicUp.DataUp', + data: data, + sum: md5, + size: fileSize, + ticket: this.sig.sigSession!, + ext: extendInfo, + encrypt: false, + timeout: timeout, + server: this.server, + port: this.port, + } as PacketHighwayTrans; + } + + async upload(cmd: number, data: ReadStream, fileSize: number, md5: Uint8Array, extendInfo: Uint8Array): Promise { + const trans = this.buildDataUpTrans(cmd, data, fileSize, md5, extendInfo); + try { + const tcpUploader = new HighwayTcpUploader(trans, this.logger); + await tcpUploader.upload(); + } catch (e) { + this.logger.logError(`[Highway] upload failed: ${e}, fallback to http upload`); + try { + const httpUploader = new HighwayHttpUploader(trans, this.logger); + await httpUploader.upload(); + } catch (e) { + this.logger.logError(`[Highway] http upload failed: ${e}`); + throw e; + } + } + } +} diff --git a/src/core/packet/highway/frame.ts b/src/core/packet/highway/frame.ts new file mode 100644 index 00000000..e0f87c4b --- /dev/null +++ b/src/core/packet/highway/frame.ts @@ -0,0 +1,23 @@ +import assert from "node:assert"; + +export class Frame{ + static pack(head: Buffer, body: Buffer): Buffer { + const totalLength = 9 + head.length + body.length + 1; + const buffer = Buffer.allocUnsafe(totalLength); + buffer[0] = 0x28; + buffer.writeUInt32BE(head.length, 1); + buffer.writeUInt32BE(body.length, 5); + head.copy(buffer, 9); + body.copy(buffer, 9 + head.length); + buffer[totalLength - 1] = 0x29; + return buffer; + } + + static unpack(frame: Buffer): [Buffer, Buffer] { + assert(frame[0] === 0x28 && frame[frame.length - 1] === 0x29, 'Invalid frame!'); + const headLen = frame.readUInt32BE(1); + const bodyLen = frame.readUInt32BE(5); + // assert(frame.length === 9 + headLen + bodyLen + 1, `Frame ${frame.toString('hex')} length does not match head and body lengths!`); + return [frame.subarray(9, 9 + headLen), frame.subarray(9 + headLen, 9 + headLen + bodyLen)]; + } +} diff --git a/src/core/packet/highway/session.ts b/src/core/packet/highway/session.ts new file mode 100644 index 00000000..946c340a --- /dev/null +++ b/src/core/packet/highway/session.ts @@ -0,0 +1,171 @@ +import * as fs from "node:fs"; +import {ChatType, Peer} from "@/core"; +import {LogWrapper} from "@/common/log"; +import {PacketClient} from "@/core/packet/client"; +import {PacketPacker} from "@/core/packet/packer"; +import {NapProtoMsg} from "@/core/packet/proto/NapProto"; +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 {int32ip2str, oidbIpv4s2HighwayIpv4s} from "@/core/packet/highway/utils"; + +export const BlockSize = 1024 * 1024; + +interface HighwayServerAddr { + ip: string + port: number +} + +export interface PacketHighwaySig { + uin: string; + sigSession: Uint8Array | null + sessionKey: Uint8Array | null + serverAddr: HighwayServerAddr[] +} + +export class PacketHighwaySession { + protected packetClient: PacketClient; + protected packetHighwayClient: PacketHighwayClient; + protected sig: PacketHighwaySig; + protected logger: LogWrapper; + protected packer: PacketPacker; + private cachedPrepareReq: Promise | null = null; + + constructor(logger: LogWrapper, client: PacketClient, packer: PacketPacker) { + this.packetClient = client; + this.logger = logger; + this.sig = { + uin: this.packetClient.napCatCore.selfInfo.uin, + sigSession: null, + sessionKey: null, + serverAddr: [], + } + this.packer = packer; + this.packetHighwayClient = new PacketHighwayClient(this.sig, this.logger); + } + + private async checkAvailable() { + if (!this.packetClient.available) { + this.logger.logError('[Highway] packetClient not available!'); + throw new Error('packetClient not available!'); + } + if (this.sig.sigSession === null || this.sig.sessionKey === null) { + this.logger.logWarn('[Highway] sigSession or sessionKey not available!'); + if (this.cachedPrepareReq === null) { + this.cachedPrepareReq = this.prepareUpload().finally(() => { + this.cachedPrepareReq = null; + }); + } + await this.cachedPrepareReq; + } + } + + private async prepareUpload(): Promise { + const packet = this.packer.packHttp0x6ff_501(); + const req = await this.packetClient.sendPacket('HttpConn.0x6ff_501', packet, true); + const rsp = new NapProtoMsg(HttpConn0x6ff_501Response).decode( + Buffer.from(req.hex_data, 'hex') + ); + this.sig.sigSession = rsp.httpConn.sigSession + this.sig.sessionKey = rsp.httpConn.sessionKey + for (const info of rsp.httpConn.serverInfos) { + if (info.serviceType !== 1) continue; + for (const addr of info.serverAddrs) { + this.logger.logDebug(`[Highway PrepareUpload] server addr add: ${int32ip2str(addr.ip)}:${addr.port}`); + this.sig.serverAddr.push({ + ip: int32ip2str(addr.ip), + port: addr.port + }) + } + } + } + + async uploadImage(peer: Peer, img: PacketMsgPicElement): Promise { + await this.checkAvailable(); + if (peer.chatType === ChatType.KCHATTYPEGROUP) { + await this.uploadGroupImageReq(Number(peer.peerUid), img); + } else if (peer.chatType === ChatType.KCHATTYPEC2C) { + await this.uploadC2CImageReq(peer.peerUid, img); + } else { + throw new Error(`[Highway] unsupported chatType: ${peer.chatType}`); + } + } + + private async uploadGroupImageReq(groupUin: number, img: PacketMsgPicElement): Promise { + const preReq = await this.packer.packUploadGroupImgReq(groupUin, img); + const preRespRaw = await this.packetClient.sendPacket('OidbSvcTrpcTcp.0x11c4_100', 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!`); + const index = preRespData.upload.msgInfo.msgInfoBody[0].index; + const sha1 = Buffer.from(index.info.fileSha1, 'hex'); + 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: [sha1] + } + }) + await this.packetHighwayClient.upload( + 1004, + fs.createReadStream(img.path, {highWaterMark: BlockSize}), + img.size, + md5, + extend + ); + } else { + this.logger.logDebug(`[Highway] 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 { + const preReq = await this.packer.packUploadC2CImgReq(peerUid, img); + const preRespRaw = await this.packetClient.sendPacket('OidbSvcTrpcTcp.0x11c5_100', 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!`); + const index = preRespData.upload.msgInfo.msgInfoBody[0].index; + const sha1 = Buffer.from(index.info.fileSha1, 'hex'); + 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: [sha1] + } + }) + await this.packetHighwayClient.upload( + 1003, + fs.createReadStream(img.path, {highWaterMark: BlockSize}), + img.size, + md5, + extend + ); + } + img.msgInfo = preRespData.upload.msgInfo; + } +} diff --git a/src/core/packet/highway/uploader.ts b/src/core/packet/highway/uploader.ts new file mode 100644 index 00000000..bef63045 --- /dev/null +++ b/src/core/packet/highway/uploader.ts @@ -0,0 +1,196 @@ +import * as net from "node:net"; +import * as crypto from "node:crypto"; +import * as http from "node:http"; +import * as stream from "node:stream"; +import {LogWrapper} from "@/common/log"; +import * as tea from "@/core/packet/utils/crypto/tea"; +import {NapProtoMsg} from "@/core/packet/proto/NapProto"; +import {ReqDataHighwayHead, RespDataHighwayHead} from "@/core/packet/proto/highway/highway"; +import {BlockSize} from "@/core/packet/highway/session"; +import {PacketHighwayTrans} from "@/core/packet/highway/client"; +import {Frame} from "@/core/packet/highway/frame"; + +abstract class HighwayUploader { + readonly trans: PacketHighwayTrans; + readonly logger: LogWrapper; + + constructor(trans: PacketHighwayTrans, logger: LogWrapper) { + this.trans = trans; + this.logger = logger; + } + + encryptTransExt(key: Uint8Array) { + if (!this.trans.encrypt) return; + this.trans.ext = tea.encrypt(Buffer.from(this.trans.ext), Buffer.from(key)); + } + + buildPicUpHead(offset: number, bodyLength: number, bodyMd5: Uint8Array): Uint8Array { + return new NapProtoMsg(ReqDataHighwayHead).encode({ + msgBaseHead: { + version: 1, + uin: this.trans.uin, + command: "PicUp.DataUp", + seq: 0, + retryTimes: 0, + appId: 1600001604, + dataFlag: 16, + commandId: this.trans.cmd, + }, + msgSegHead: { + serviceId: 0, + filesize: BigInt(this.trans.size), + dataOffset: BigInt(offset), + dataLength: bodyLength, + serviceTicket: this.trans.ticket, + md5: bodyMd5, + fileMd5: this.trans.sum, + cacheAddr: 0, + cachePort: 0, + }, + bytesReqExtendInfo: this.trans.ext, + timestamp: BigInt(0), + msgLoginSigHead: { + uint32LoginSigType: 8, + appId: 1600001604, + } + }) + } + + abstract upload(): Promise; +} + +class HighwayTcpUploaderTransform extends stream.Transform { + uploader: HighwayTcpUploader; + offset: number; + + constructor(uploader: HighwayTcpUploader) { + super(); + this.uploader = uploader; + this.offset = 0; + } + + _transform(data: Buffer, _: BufferEncoding, callback: stream.TransformCallback) { + let chunkOffset = 0; + while (chunkOffset < data.length) { + const chunkSize = Math.min(BlockSize, data.length - chunkOffset); + const chunk = data.subarray(chunkOffset, chunkOffset + chunkSize); + const chunkMd5 = crypto.createHash('md5').update(chunk).digest(); + const head = this.uploader.buildPicUpHead(this.offset, chunk.length, chunkMd5); + chunkOffset += chunk.length; + this.offset += chunk.length; + this.push(Frame.pack(Buffer.from(head), chunk)); + } + callback(null); + } +} + +export class HighwayTcpUploader extends HighwayUploader { + async upload(): Promise { + const highwayTransForm = new HighwayTcpUploaderTransform(this); + const upload = new Promise((resolve, _) => { + const socket = net.connect(this.trans.port, this.trans.server, () => { + 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})`); + } + 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')}`); + if (Number(rsp.msgSegHead?.dataOffset) + Number(rsp.msgSegHead?.dataLength) >= Number(rsp.msgSegHead?.filesize)) { + this.logger.logDebug('[Highway] tcpUpload finished.'); + socket.end(); + resolve(); + } + }; + socket.on('data', (chunk: Buffer) => { + try { + 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); + }) + this.trans.data.on('error', (err) => { + this.logger.logError('[Highway] tcpUpload readable error:', err); + socket.end(); + }) + }) + const timeout = new Promise((_, reject) => { + setTimeout(() => { + reject(new Error(`[Highway] tcpUpload timeout after ${this.trans.timeout}s`)) + }, (this.trans.timeout ?? Infinity) * 1000 + ) + }) + await Promise.race([upload, timeout]); + } +} + +// TODO: timeout impl +export class HighwayHttpUploader extends HighwayUploader { + async upload(): Promise { + let offset = 0; + for await (const chunk of this.trans.data) { + let 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; + } + offset += block.length; + } + } + + private async uploadBlock(block: Buffer, offset: number): Promise { + const chunkMD5 = crypto.createHash('md5').update(block).digest(); + const payload = this.buildPicUpHead(offset, block.length, chunkMD5); + const frame = Frame.pack(Buffer.from(payload), block) + const resp = await this.httpPostHighwayContent(frame, `http://${this.trans.server}:${this.trans.port}/cgi-bin/httpconn?htcmd=0x6FF0087&uin=${this.trans.uin}`); + 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})`); + } + } + + private async httpPostHighwayContent(frame: Buffer, serverURL: string): Promise { + return new Promise((resolve, reject) => { + try { + const options: http.RequestOptions = { + method: 'POST', + headers: { + 'Connection': 'keep-alive', + 'Accept-Encoding': 'identity', + 'User-Agent': 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2)', + 'Content-Length': frame.length.toString(), + }, + }; + const req = http.request(serverURL, options, (res) => { + let data = Buffer.alloc(0); + res.on('data', (chunk) => { + data = Buffer.concat([data, chunk]); + }); + res.on('end', () => { + resolve(data); + }); + }); + req.write(frame); + req.on('error', (error) => { + reject(error); + }); + } catch (error) { + reject(error); + } + }); + } +} diff --git a/src/core/packet/highway/utils.ts b/src/core/packet/highway/utils.ts new file mode 100644 index 00000000..65e0be44 --- /dev/null +++ b/src/core/packet/highway/utils.ts @@ -0,0 +1,20 @@ +import {NapProtoEncodeStructType} from "@/core/packet/proto/NapProto"; +import {IPv4} from "@/core/packet/proto/oidb/common/Ntv2.RichMediaResp"; +import {NTHighwayIPv4} from "@/core/packet/proto/highway/highway"; + +export const int32ip2str = (ip: number) => { + ip = ip & 0xffffffff; + return [ip & 0xff, (ip & 0xff00) >> 8, (ip & 0xff0000) >> 16, ((ip & 0xff000000) >> 24) & 0xff].join('.'); +} + +export const oidbIpv4s2HighwayIpv4s = (ipv4s: NapProtoEncodeStructType[]): NapProtoEncodeStructType[] =>{ + return ipv4s.map((ip) => { + return { + domain: { + isEnable: true, + ip: int32ip2str(ip.outIP!), + }, + port: ip.outPort! + } as NapProtoEncodeStructType + }) +} diff --git a/src/core/packet/msg/builder.ts b/src/core/packet/msg/builder.ts new file mode 100644 index 00000000..a169cb3f --- /dev/null +++ b/src/core/packet/msg/builder.ts @@ -0,0 +1,58 @@ +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"; + +export class PacketMsgBuilder { + private logger: LogWrapper; + + constructor(logger: LogWrapper) { + this.logger = logger; + } + + buildFakeMsg(selfUid: string, element: PacketMsg[]): NapProtoEncodeStructType[] { + return element.map((node): NapProtoEncodeStructType => { + const avatar = `https://q.qlogo.cn/headimg_dl?dst_uin=${node.senderUin}&spec=640&img_type=jpg`; + const msgElement = node.msg.flatMap(msg => msg.buildElement() ?? []); + return { + responseHead: { + fromUid: "", + fromUin: node.senderUin, + toUid: node.groupId ? undefined : selfUid, + forward: node.groupId ? undefined : { + friendName: node.senderName, + }, + grp: node.groupId ? { + groupUin: node.groupId, + memberName: node.senderName, + unknown5: 2 + } : undefined, + }, + contentHead: { + type: node.groupId ? 82 : 9, + subType: node.groupId ? undefined : 4, + divSeq: node.groupId ? undefined : 4, + msgId: crypto.randomBytes(4).readUInt32LE(0), + sequence: crypto.randomBytes(4).readUInt32LE(0), + timeStamp: Math.floor(Date.now() / 1000), + field7: BigInt(1), + field8: 0, + field9: 0, + forward: { + field1: 0, + field2: 0, + field3: node.groupId ? 0 : 2, + unknownBase64: avatar, + avatar: avatar + } + }, + body: { + richText: { + elems: msgElement + } + } + }; + }); + } +} diff --git a/src/core/packet/msg/converter.ts b/src/core/packet/msg/converter.ts new file mode 100644 index 00000000..3cd4b8b2 --- /dev/null +++ b/src/core/packet/msg/converter.ts @@ -0,0 +1,131 @@ +import { + MessageElement, + RawMessage, + SendArkElement, + SendFaceElement, + SendFileElement, + SendMarkdownElement, + SendMarketFaceElement, + SendPicElement, + SendPttElement, + SendReplyElement, + SendStructLongMsgElement, + SendTextElement, + SendVideoElement +} from "@/core"; +import { + IPacketMsgElement, + PacketMsgAtElement, + PacketMsgFaceElement, + PacketMsgFileElement, + PacketMsgLightAppElement, + PacketMsgMarkDownElement, + PacketMsgMarkFaceElement, + PacketMsgPicElement, + PacketMsgPttElement, + PacketMsgReplyElement, + PacketMsgTextElement, + PacketMsgVideoElement, + PacketMultiMsgElement +} from "@/core/packet/msg/element"; +import {PacketMsg, PacketSendMsgElement} from "@/core/packet/msg/message"; +import {LogWrapper} from "@/common/log"; + +type SendMessageElementMap = { + textElement: SendTextElement, + picElement: SendPicElement, + replyElement: SendReplyElement, + faceElement: SendFaceElement, + marketFaceElement: SendMarketFaceElement, + videoElement: SendVideoElement, + fileElement: SendFileElement, + pttElement: SendPttElement, + arkElement: SendArkElement, + markdownElement: SendMarkdownElement, + structLongMsgElement: SendStructLongMsgElement +}; + +type RawToPacketMsgConverters = { + [K in keyof SendMessageElementMap]: ( + element: SendMessageElementMap[K], + msg?: RawMessage, + elementWrapper?: MessageElement, + ) => IPacketMsgElement | null; +}; + +export type rawMsgWithSendMsg = { + senderUin: number; + senderUid?: string; + senderName: string; + groupId?: number; + time: number; + msg: PacketSendMsgElement[] +} + +export class PacketMsgConverter { + private logger: LogWrapper; + + constructor(logger: LogWrapper) { + this.logger = logger; + } + + rawMsgWithSendMsgToPacketMsg(msg: rawMsgWithSendMsg): PacketMsg { + return { + senderUid: msg.senderUid ?? '', + senderUin: msg.senderUin, + senderName: msg.senderName, + groupId: msg.groupId, + time: msg.time, + msg: msg.msg.map((element) => { + const key = (Object.keys(this.rawToPacketMsgConverters) as Array).find( + (k) => (element as any)[k] !== undefined // TODO: + ); + if (key) { + const elementData = (element as any)[key]; // TODO: + if (elementData) return this.rawToPacketMsgConverters[key](element as any) + } + return null; + }).filter((e) => e !== null) + } + } + + private rawToPacketMsgConverters: RawToPacketMsgConverters = { + textElement: (element: SendTextElement) => { + if (element.textElement.atType) { + return new PacketMsgAtElement(element) + } + return new PacketMsgTextElement(element) + }, + picElement: (element: SendPicElement) => { + return new PacketMsgPicElement(element) + }, + replyElement: (element: SendReplyElement) => { + return new PacketMsgReplyElement(element) + }, + faceElement: (element: SendFaceElement) => { + return new PacketMsgFaceElement(element) + }, + marketFaceElement: (element: SendMarketFaceElement) => { + return new PacketMsgMarkFaceElement(element) + }, + videoElement: (element: SendVideoElement) => { + return new PacketMsgVideoElement(element) + }, + fileElement: (element: SendFileElement) => { + return new PacketMsgFileElement(element) + }, + pttElement: (element: SendPttElement) => { + return new PacketMsgPttElement(element) + }, + arkElement: (element: SendArkElement) => { + return new PacketMsgLightAppElement(element) + }, + markdownElement: (element: SendMarkdownElement) => { + return new PacketMsgMarkDownElement(element) + }, + // TODO: check this logic, move it in arkElement? + structLongMsgElement: (element: SendStructLongMsgElement) => { + return new PacketMultiMsgElement(element) + } + } +} diff --git a/src/core/packet/msg/element.ts b/src/core/packet/msg/element.ts new file mode 100644 index 00000000..797b4a7b --- /dev/null +++ b/src/core/packet/msg/element.ts @@ -0,0 +1,415 @@ +import assert from "node:assert"; +import * as zlib from "node:zlib"; +import * as crypto from "node:crypto"; +import {NapProtoEncodeStructType, NapProtoMsg} from "@/core/packet/proto/NapProto"; +import { + CustomFace, + Elem, + MarkdownData, + MentionExtra, + NotOnlineImage, + QBigFaceExtra, + QSmallFaceExtra +} from "@/core/packet/proto/message/element"; +import { + AtType, + PicType, + SendArkElement, + SendFaceElement, + SendFileElement, + SendMarkdownElement, + SendMarketFaceElement, + SendPicElement, + SendPttElement, + SendReplyElement, + SendStructLongMsgElement, + SendTextElement, + SendVideoElement +} from "@/core"; +import {MsgInfo} from "@/core/packet/proto/oidb/common/Ntv2.RichMediaReq"; +import {PacketMsg, PacketSendMsgElement} from "@/core/packet/msg/message"; + +// raw <-> packet +// TODO: check ob11 -> raw impl! +// TODO: parse to raw element +// TODO: SendStructLongMsgElement +export abstract class IPacketMsgElement { + protected constructor(rawElement: T) { + } + + buildContent(): Uint8Array | undefined { + return undefined; + } + + buildElement(): NapProtoEncodeStructType[] | undefined { + return undefined; + } + + toPreview(): string { + return '[nya~]'; + } +} + +export class PacketMsgTextElement extends IPacketMsgElement { + text: string; + + constructor(element: SendTextElement) { + super(element); + this.text = element.textElement.content; + } + + buildElement(): NapProtoEncodeStructType[] { + return [{ + text: { + str: this.text + } + }]; + } + + toPreview(): string { + return this.text; + } +} + +export class PacketMsgAtElement extends PacketMsgTextElement { + targetUid: string; + atAll: boolean; + + constructor(element: SendTextElement) { + super(element); + this.targetUid = element.textElement.atNtUid; + this.atAll = element.textElement.atType === AtType.atAll; + } + + buildElement(): NapProtoEncodeStructType[] { + return [{ + text: { + str: this.text, + pbReserve: new NapProtoMsg(MentionExtra).encode({ + type: this.atAll ? 1 : 2, + uin: 0, + field5: 0, + uid: this.targetUid, + } + ) + } + }]; + } + + toPreview(): string { + return `@${this.targetUid} ${this.text}`; + } +} + +export class PacketMsgPicElement extends IPacketMsgElement { + path: string; + name: string + size: number; + md5: string; + width: number; + height: number; + picType: PicType; + sha1: string | null = null; + msgInfo: NapProtoEncodeStructType | null = null; + groupPicExt: NapProtoEncodeStructType | null = null; + c2cPicExt: NapProtoEncodeStructType | null = null; + + constructor(element: SendPicElement) { + super(element); + this.path = element.picElement.sourcePath; + this.name = element.picElement.fileName; + this.size = Number(element.picElement.fileSize); + this.md5 = element.picElement.md5HexStr ?? ''; + this.width = element.picElement.picWidth; + this.height = element.picElement.picHeight; + this.picType = element.picElement.picType; + } + + buildElement(): NapProtoEncodeStructType[] { + assert(this.msgInfo !== null, 'msgInfo is null, expected not null'); + return [{ + commonElem: { + serviceType: 48, + pbElem: new NapProtoMsg(MsgInfo).encode(this.msgInfo), + businessType: 10, + } + }] + } + + toPreview(): string { + return "[图片]"; + } +} + +export class PacketMsgReplyElement extends IPacketMsgElement { + messageId: bigint; + messageSeq: number; + messageClientSeq: number; + targetUin: number; + targetUid: string; + time: number; + elems: PacketMsg[]; + + 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.targetUid = element.replyElement.senderUidStr ?? ''; + this.time = Number(element.replyElement.replyMsgTime ?? 0); + this.elems = []; // TODO: in replyElement.sourceMsgTextElems + } + + get isGroupReply(): boolean { + return this.messageClientSeq !== 0; + } + + buildElement(): NapProtoEncodeStructType[] { + return [{ + srcMsg: { + origSeqs: [this.isGroupReply ? this.messageClientSeq : this.messageSeq], + senderUin: BigInt(this.targetUin), + time: this.time, + elems: [], // TODO: in replyElement.sourceMsgTextElems + pbReserve: { + messageId: this.messageId, + }, + toUin: BigInt(0), + } + }, { + text: this.isGroupReply ? { + str: 'nya~', + pbReserve: new NapProtoMsg(MentionExtra).encode({ + type: this.targetUin === 0 ? 1 : 2, + uin: 0, + field5: 0, + uid: String(this.targetUid), + }), + } : undefined, + }] + } + + toPreview(): string { + return "[回复]"; + } +} + +export class PacketMsgFaceElement extends IPacketMsgElement { + faceId: number; + isLargeFace: boolean; + + constructor(element: SendFaceElement) { + super(element); + this.faceId = element.faceElement.faceIndex; + this.isLargeFace = element.faceElement.faceType === 3; + } + + buildElement(): NapProtoEncodeStructType[] { + if (this.isLargeFace) { + return [{ + commonElem: { + serviceType: 37, + pbElem: new NapProtoMsg(QBigFaceExtra).encode({ + aniStickerPackId: "1", + aniStickerId: "8", + faceId: this.faceId, + field4: 1, + field6: "", + preview: "", + field9: 1 + }), + businessType: 1 + } + }] + } else if (this.faceId < 260) { + return [{ + face: { + index: this.faceId + } + }]; + } else { + return [{ + commonElem: { + serviceType: 33, + pbElem: new NapProtoMsg(QSmallFaceExtra).encode({ + faceId: this.faceId, + preview: "", + preview2: "" + }), + businessType: 1 + } + }] + } + } + + toPreview(): string { + return "[表情]"; + } +} + +export class PacketMsgMarkFaceElement extends IPacketMsgElement { + emojiName: string; + emojiId: string; + emojiPackageId: number; + emojiKey: string; + + constructor(element: SendMarketFaceElement) { + super(element); + this.emojiName = element.marketFaceElement.faceName; + this.emojiId = element.marketFaceElement.emojiId; + this.emojiPackageId = element.marketFaceElement.emojiPackageId; + this.emojiKey = element.marketFaceElement.key; + } + + buildElement(): NapProtoEncodeStructType[] { + return [{ + marketFace: { + faceName: this.emojiName, + itemType: 6, + faceInfo: 1, + faceId: Buffer.from(this.emojiId, 'hex'), + tabId: this.emojiPackageId, + subType: 3, + key: this.emojiKey, + imageWidth: 300, + imageHeight: 300, + pbReserve: { + field8: 1 + } + } + }] + } + + toPreview(): string { + return this.emojiName; + } +} + +export class PacketMsgVideoElement extends IPacketMsgElement { + constructor(element: SendVideoElement) { + super(element); + } +} + +export class PacketMsgFileElement extends IPacketMsgElement { + constructor(element: SendFileElement) { + super(element); + } +} + +export class PacketMsgPttElement extends IPacketMsgElement { + constructor(element: SendPttElement) { + super(element); + } +} + +export class PacketMsgLightAppElement extends IPacketMsgElement { + payload: string; + + constructor(element: SendArkElement) { + super(element); + this.payload = element.arkElement.bytesData; + } + + buildElement(): NapProtoEncodeStructType[] { + return [{ + lightAppElem: { + data: Buffer.concat([ + Buffer.from([0x01]), + zlib.deflateSync(Buffer.from(this.payload, 'utf-8')) + ]) + } + }] + } + + toPreview(): string { + return "[小程序]"; + } +} + +export class PacketMsgMarkDownElement extends IPacketMsgElement { + content: string; + + constructor(element: SendMarkdownElement) { + super(element); + this.content = element.markdownElement.content; + } + + buildElement(): NapProtoEncodeStructType[] { + return [{ + commonElem: { + serviceType: 45, + pbElem: new NapProtoMsg(MarkdownData).encode({ + content: this.content + }), + businessType: 1 + } + }] + } + + toPreview(): string { + return this.content; + } +} + +export class PacketMultiMsgElement extends IPacketMsgElement { + resid: string; + message: PacketMsg[]; + + constructor(rawElement: SendStructLongMsgElement, message?: PacketMsg[]) { + super(rawElement); + this.resid = rawElement.structLongMsgElement.resId; + this.message = message ?? []; + } + + get JSON() { + const id = crypto.randomUUID(); + return { + app: "com.tencent.multimsg", + config: { + autosize: 1, + forward: 1, + round: 1, + type: "normal", + width: 300 + }, + desc: "[聊天记录]", + extra: { + filename: id, + tsum: this.message.length, + }, + meta: { + detail: { + news: this.message.length === 0 ? [{ + text: "[Nya~ This message is send from NapCat.Packet!]", + }] : this.message.map(packetMsg => ({ + text: `${packetMsg.senderName}: ${packetMsg.msg.map(msg => msg.toPreview()).join('')}`, + })), + resid: this.resid, + source: "聊天记录", // TODO: + summary: `查看${this.message.length}条转发消息`, + uniseq: id, + } + }, + prompt: "[聊天记录]", + ver: "0.0.0.5", + view: "contact", + } + } + + buildElement(): NapProtoEncodeStructType[] { + return [{ + lightAppElem: { + data: Buffer.concat([ + Buffer.from([0x01]), + zlib.deflateSync(Buffer.from(JSON.stringify(this.JSON), 'utf-8')) + ]) + } + }] + } + + toPreview(): string { + return "[聊天记录]"; + } +} diff --git a/src/core/packet/msg/message.ts b/src/core/packet/msg/message.ts new file mode 100644 index 00000000..5ce10f54 --- /dev/null +++ b/src/core/packet/msg/message.ts @@ -0,0 +1,15 @@ +import {IPacketMsgElement} from "@/core/packet/msg/element"; +import {SendMessageElement, SendStructLongMsgElement} from "@/core"; + +export type PacketSendMsgElement = SendMessageElement | SendStructLongMsgElement + +export interface PacketMsg { + seq?: number; + clientSeq?: number; + groupId?: number; + senderUid: string; + senderUin: number; + senderName: string; + time: number; + msg: IPacketMsgElement[] +} diff --git a/src/core/packet/packer.ts b/src/core/packet/packer.ts new file mode 100644 index 00000000..55e57458 --- /dev/null +++ b/src/core/packet/packer.ts @@ -0,0 +1,326 @@ +import * as zlib from "node:zlib"; +import * as crypto from "node:crypto"; +import {calculateSha1} 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"; +import {OidbSvcTrpcTcp0X8FC_2, OidbSvcTrpcTcp0X8FC_2_Body} from "@/core/packet/proto/oidb/Oidb.0x8FC_2"; +import {OidbSvcTrpcTcp0XFE1_2} from "@/core/packet/proto/oidb/Oidb.0XFE1_2"; +import {OidbSvcTrpcTcp0XED3_1} from "@/core/packet/proto/oidb/Oidb.0xED3_1"; +import {NTV2RichMediaReq} from "@/core/packet/proto/oidb/common/Ntv2.RichMediaReq"; +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 {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"; + +export type PacketHexStr = string & { readonly hexNya: unique symbol }; + +export class PacketPacker { + readonly logger: LogWrapper; + readonly client: PacketClient; + readonly packetBuilder: PacketMsgBuilder; + readonly packetConverter: PacketMsgConverter; + + constructor(logger: LogWrapper, client: PacketClient) { + this.logger = logger; + this.client = client; + this.packetBuilder = new PacketMsgBuilder(logger); + this.packetConverter = new PacketMsgConverter(logger); + } + + private toHexStr(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({ + command: cmd, + subCommand: subCmd, + body: body, + isReserved: isUid ? 1 : 0 + }); + } + + packPokePacket(group: number, peer: number): PacketHexStr { + const oidb_0xed3 = new NapProtoMsg(OidbSvcTrpcTcp0XED3_1).encode({ + uin: peer, + groupUin: group, + friendUin: group, + ext: 0 + }); + return this.toHexStr(this.packOidbPacket(0xed3, 1, oidb_0xed3)); + } + + packRkeyPacket(): PacketHexStr { + const oidb_0x9067_202 = new NapProtoMsg(OidbSvcTrpcTcp0X9067_202).encode({ + reqHead: { + common: { + requestId: 1, + command: 202 + }, + scene: { + requestType: 2, + businessType: 1, + sceneType: 0 + }, + client: { + agentType: 2 + } + }, + downloadRKeyReq: { + key: [10, 20, 2] + }, + }); + return this.toHexStr(this.packOidbPacket(0x9067, 202, oidb_0x9067_202)); + } + + packSetSpecialTittlePacket(groupCode: string, uid: string, tittle: string): PacketHexStr { + const oidb_0x8FC_2_body = new NapProtoMsg(OidbSvcTrpcTcp0X8FC_2_Body).encode({ + targetUid: uid, + specialTitle: tittle, + expiredTime: -1, + uinName: tittle + }); + const oidb_0x8FC_2 = new NapProtoMsg(OidbSvcTrpcTcp0X8FC_2).encode({ + groupUin: +groupCode, + body: oidb_0x8FC_2_body + }); + return this.toHexStr(this.packOidbPacket(0x8FC, 2, oidb_0x8FC_2, false, false)); + } + + packStatusPacket(uin: number): PacketHexStr { + const oidb_0xfe1_2 = new NapProtoMsg(OidbSvcTrpcTcp0XFE1_2).encode({ + uin: uin, + key: [{key: 27372}] + }); + return this.toHexStr(this.packOidbPacket(0xfe1, 2, oidb_0xfe1_2)); + } + + async packUploadForwardMsg(selfUid: string, msg: PacketMsg[], groupUin: number = 0): Promise { + const msgBody = this.packetBuilder.buildFakeMsg(selfUid, msg); + const longMsgResultData = new NapProtoMsg(LongMsgResult).encode( + { + action: { + actionCommand: "MultiMsg", + actionData: { + msgBody: msgBody + } + } + } + ); + this.logger.logDebug("packUploadForwardMsg LONGMSGRESULT!!!", this.toHexStr(longMsgResultData)); + const payload = zlib.gzipSync(Buffer.from(longMsgResultData)); + // this.logger.logDebug("packUploadForwardMsg PAYLOAD!!!", payload); + const req = new NapProtoMsg(SendLongMsgReq).encode( + { + info: { + type: groupUin === 0 ? 1 : 3, + uid: { + uid: groupUin === 0 ? selfUid : groupUin.toString(), + }, + groupUin: groupUin, + payload: payload + }, + settings: { + field1: 4, field2: 1, field3: 7, field4: 0 + } + } + ); + // this.logger.logDebug("packUploadForwardMsg REQ!!!", req); + return this.toHexStr(req); + } + + // highway part + packHttp0x6ff_501(): PacketHexStr { + return this.toHexStr(new NapProtoMsg(HttpConn0x6ff_501).encode({ + httpConn: { + field1: 0, + field2: 0, + field3: 16, + field4: 1, + field6: 3, + serviceTypes: [1, 5, 10, 21], + // tgt: "", // TODO: do we really need tgt? seems not + field9: 2, + field10: 9, + field11: 8, + ver: "1.0.1" + } + })); + } + + async packUploadGroupImgReq(groupUin: number, img: PacketMsgPicElement): Promise { + const req = new NapProtoMsg(NTV2RichMediaReq).encode( + { + reqHead: { + common: { + requestId: 1, + command: 100 + }, + scene: { + requestType: 2, + businessType: 1, + sceneType: 2, + group: { + groupUin: groupUin + }, + }, + client: { + agentType: 2 + } + }, + upload: { + uploadInfo: [ + { + fileInfo: { + fileSize: Number(img.size), + fileHash: img.md5, + fileSha1: this.toHexStr(await calculateSha1(img.path)), + fileName: img.name, + type: { + type: 1, + picFormat: img.picType, //TODO: extend NapCat imgType /cc @MliKiowa + videoFormat: 0, + voiceFormat: 0, + }, + width: img.width, + height: img.height, + time: 0, + original: 1 + }, + subFileType: 0, + } + ], + tryFastUploadCompleted: true, + srvSendMsg: false, + clientRandomId: crypto.randomBytes(8).readBigUInt64BE() & BigInt('0x7FFFFFFFFFFFFFFF'), + compatQMsgSceneType: 2, + extBizInfo: { + pic: { + bytesPbReserveTroop: Buffer.from("0800180020004200500062009201009a0100a2010c080012001800200028003a00", 'hex'), + textSummary: "Nya~", // TODO: + }, + video: { + bytesPbReserve: Buffer.alloc(0), + }, + ptt: { + bytesPbReserve: Buffer.alloc(0), + bytesReserve: Buffer.alloc(0), + bytesGeneralFlags: Buffer.alloc(0), + } + }, + clientSeq: 0, + noNeedCompatMsg: false, + } + } + ) + return this.toHexStr(this.packOidbPacket(0x11c4, 100, req, true, false)); + } + + async packUploadC2CImgReq(peerUin: string, img: PacketMsgPicElement): Promise { + const req = new NapProtoMsg(NTV2RichMediaReq).encode({ + reqHead: { + common: { + requestId: 1, + command: 100 + }, + scene: { + requestType: 2, + businessType: 1, + sceneType: 1, + c2C: { + accountType: 2, + targetUid: peerUin + }, + }, + client: { + agentType: 2, + } + }, + upload: { + uploadInfo: [ + { + fileInfo: { + fileSize: Number(img.size), + fileHash: img.md5, + fileSha1: this.toHexStr(await calculateSha1(img.path)), + fileName: img.name, + type: { + type: 1, + picFormat: img.picType, //TODO: extend NapCat imgType /cc @MliKiowa + videoFormat: 0, + voiceFormat: 0, + }, + width: img.width, + height: img.height, + time: 0, + original: 1 + }, + subFileType: 0, + } + ], + tryFastUploadCompleted: true, + srvSendMsg: false, + clientRandomId: crypto.randomBytes(8).readBigUInt64BE() & BigInt('0x7FFFFFFFFFFFFFFF'), + compatQMsgSceneType: 1, + extBizInfo: { + pic: { + bytesPbReserveTroop: Buffer.from("0800180020004200500062009201009a0100a2010c080012001800200028003a00", 'hex'), + textSummary: "Nya~", // TODO: + }, + video: { + bytesPbReserve: Buffer.alloc(0), + }, + ptt: { + bytesPbReserve: Buffer.alloc(0), + bytesReserve: Buffer.alloc(0), + bytesGeneralFlags: Buffer.alloc(0), + } + }, + clientSeq: 0, + noNeedCompatMsg: false, + } + } + ) + return this.toHexStr(this.packOidbPacket(0x11c5, 100, req, true, false)); + } + + packGroupFileDownloadReq(groupUin: number, fileUUID: string): PacketHexStr { + return this.toHexStr( + this.packOidbPacket(0x6D6, 2, new NapProtoMsg(OidbSvcTrpcTcp0x6D6).encode({ + download: { + groupUin: groupUin, + appId: 7, + busId: 102, + fileId: fileUUID + } + }), true, false) + ) + } + + packC2CFileDownloadReq(selfUid: string, fileUUID: string, fileHash: string): PacketHexStr { + return this.toHexStr( + new NapProtoMsg(OidbSvcTrpcTcp0XE37_1200).encode({ + subCommand: 1200, + field2: 1, + body: { + receiverUid: selfUid, + fileUuid: fileUUID, + type: 2, + fileHash: fileHash, + t2: 0 + }, + field101: 3, + field102: 103, + field200: 1, + field99999: Buffer.from([0xc0, 0x85, 0x2c, 0x01]) + }) + ) + } +} diff --git a/src/core/packet/proto/NapProto.ts b/src/core/packet/proto/NapProto.ts new file mode 100644 index 00000000..ca9d7132 --- /dev/null +++ b/src/core/packet/proto/NapProto.ts @@ -0,0 +1,139 @@ +import { MessageType, PartialMessage, RepeatType, ScalarType } from '@protobuf-ts/runtime'; +import { PartialFieldInfo } from "@protobuf-ts/runtime/build/types/reflection-info"; + +type LowerCamelCase = CamelCaseHelper; + +type CamelCaseHelper< + S extends string, + CapNext extends boolean, + IsFirstChar extends boolean +> = S extends `${infer F}${infer R}` + ? F extends '_' + ? CamelCaseHelper + : F extends `${number}` + ? `${F}${CamelCaseHelper}` + : CapNext extends true + ? `${Uppercase}${CamelCaseHelper}` + : IsFirstChar extends true + ? `${Lowercase}${CamelCaseHelper}` + : `${F}${CamelCaseHelper}` + : ''; + +type ScalarTypeToTsType = + T extends ScalarType.DOUBLE | ScalarType.FLOAT | ScalarType.INT32 | ScalarType.FIXED32 | ScalarType.UINT32 | ScalarType.SFIXED32 | ScalarType.SINT32 ? number : + T extends ScalarType.INT64 | ScalarType.UINT64 | ScalarType.FIXED64 | ScalarType.SFIXED64 | ScalarType.SINT64 ? bigint : + T extends ScalarType.BOOL ? boolean : + T extends ScalarType.STRING ? string : + T extends ScalarType.BYTES ? Uint8Array : + never; + +interface BaseProtoFieldType { + kind: 'scalar' | 'message'; + no: number; + type: T; + optional: O; + repeat: R; +} + +interface ScalarProtoFieldType extends BaseProtoFieldType { + kind: 'scalar'; +} + +interface MessageProtoFieldType ProtoMessageType, O extends boolean, R extends O extends true ? false : boolean> extends BaseProtoFieldType { + kind: 'message'; +} + +type ProtoFieldType = + | ScalarProtoFieldType + | MessageProtoFieldType<() => ProtoMessageType, boolean, boolean>; + +type ProtoMessageType = { + [key: string]: ProtoFieldType; +}; + +export function ProtoField(no: number, type: T, optional?: O, repeat?: R): ScalarProtoFieldType; +export function ProtoField ProtoMessageType, O extends boolean = false, R extends O extends true ? false : boolean = false>(no: number, type: T, optional?: O, repeat?: R): MessageProtoFieldType; +export function ProtoField(no: number, type: ScalarType | (() => ProtoMessageType), optional?: boolean, repeat?: boolean): ProtoFieldType { + if (typeof type === 'function') { + return { kind: 'message', no: no, type: type, optional: optional ?? false, repeat: repeat ?? false }; + } else { + return { kind: 'scalar', no: no, type: type, optional: optional ?? false, repeat: repeat ?? false }; + } +} + +type ProtoFieldReturnType = NonNullable extends ScalarProtoFieldType + ? ScalarTypeToTsType + : T extends NonNullable> + ? NonNullable, E>> + : never; + +type RequiredFieldsBaseType = { + [K in keyof T as T[K] extends { optional: true } ? never : LowerCamelCase]: + T[K] extends { repeat: true } + ? ProtoFieldReturnType[] + : ProtoFieldReturnType +} + +type OptionalFieldsBaseType = { + [K in keyof T as T[K] extends { optional: true } ? LowerCamelCase : never]?: + T[K] extends { repeat: true } + ? ProtoFieldReturnType[] + : ProtoFieldReturnType +} + +type RequiredFieldsType = E extends true ? Partial> : RequiredFieldsBaseType; + +type OptionalFieldsType = E extends true ? Partial> : OptionalFieldsBaseType; + +type NapProtoStructType = RequiredFieldsType & OptionalFieldsType; + +export type NapProtoEncodeStructType = NapProtoStructType; + +export type NapProtoDecodeStructType = NapProtoStructType; + +const NapProtoMsgCache = new Map>>(); + +export class NapProtoMsg { + private readonly _msg: T; + private readonly _field: PartialFieldInfo[]; + private readonly _proto_msg: MessageType>; + + constructor(fields: T) { + this._msg = fields; + this._field = Object.keys(fields).map(key => { + const field = fields[key]; + if (field.kind === 'scalar') { + const repeatType = field.repeat + ? [ScalarType.STRING, ScalarType.BYTES].includes(field.type) + ? RepeatType.UNPACKED + : RepeatType.PACKED + : RepeatType.NO; + return { + no: field.no, + name: key, + kind: 'scalar', + T: field.type, + opt: field.optional, + repeat: repeatType, + }; + } else if (field.kind === 'message') { + return { + no: field.no, + name: key, + kind: 'message', + repeat: field.repeat ? RepeatType.PACKED : RepeatType.NO, + T: () => new NapProtoMsg(field.type())._proto_msg, + }; + } + }) as PartialFieldInfo[]; + this._proto_msg = new MessageType>('nya', this._field); + } + + encode(data: NapProtoEncodeStructType): Uint8Array { + return this._proto_msg.toBinary(this._proto_msg.create(data as PartialMessage>)); + } + + decode(data: Uint8Array): NapProtoDecodeStructType { + return this._proto_msg.fromBinary(data) as NapProtoDecodeStructType; + } +} diff --git a/src/core/packet/proto/action/action.ts b/src/core/packet/proto/action/action.ts new file mode 100644 index 00000000..3d4c9e44 --- /dev/null +++ b/src/core/packet/proto/action/action.ts @@ -0,0 +1,114 @@ +import { ScalarType } from "@protobuf-ts/runtime"; +import { ProtoField } from "../NapProto"; +import {ContentHead, MessageBody, MessageControl, RoutingHead} from "@/core/packet/proto/message/message"; + +export const FaceRoamRequest = { + comm: ProtoField(1, () => PlatInfo, true), + selfUin: ProtoField(2, ScalarType.UINT32), + subCmd: ProtoField(3, ScalarType.UINT32), + field6: ProtoField(6, ScalarType.UINT32), +}; + +export const PlatInfo = { + imPlat: ProtoField(1, ScalarType.UINT32), + osVersion: ProtoField(2, ScalarType.STRING, true), + qVersion: ProtoField(3, ScalarType.STRING, true), +}; + +export const FaceRoamResponse = { + retCode: ProtoField(1, ScalarType.UINT32), + errMsg: ProtoField(2, ScalarType.STRING), + subCmd: ProtoField(3, ScalarType.UINT32), + userInfo: ProtoField(6, () => FaceRoamUserInfo), +}; + +export const FaceRoamUserInfo = { + fileName: ProtoField(1, ScalarType.STRING, false, true), + deleteFile: ProtoField(2, ScalarType.STRING, false, true), + bid: ProtoField(3, ScalarType.STRING), + maxRoamSize: ProtoField(4, ScalarType.UINT32), + emojiType: ProtoField(5, ScalarType.UINT32, false, true), +}; + +export const SendMessageRequest = { + state: ProtoField(1, ScalarType.INT32), + sizeCache: ProtoField(2, ScalarType.INT32), + unknownFields: ProtoField(3, ScalarType.BYTES), + routingHead: ProtoField(4, () => RoutingHead), + contentHead: ProtoField(5, () => ContentHead), + messageBody: ProtoField(6, () => MessageBody), + msgSeq: ProtoField(7, ScalarType.INT32), + msgRand: ProtoField(8, ScalarType.INT32), + syncCookie: ProtoField(9, ScalarType.BYTES), + msgVia: ProtoField(10, ScalarType.INT32), + dataStatist: ProtoField(11, ScalarType.INT32), + messageControl: ProtoField(12, () => MessageControl), + multiSendSeq: ProtoField(13, ScalarType.INT32), +}; + +export const SendMessageResponse = { + result: ProtoField(1, ScalarType.INT32), + errMsg: ProtoField(2, ScalarType.STRING, true), + timestamp1: ProtoField(3, ScalarType.UINT32), + field10: ProtoField(10, ScalarType.UINT32), + groupSequence: ProtoField(11, ScalarType.UINT32, true), + timestamp2: ProtoField(12, ScalarType.UINT32), + privateSequence: ProtoField(14, ScalarType.UINT32), +}; + +export const SetStatus = { + status: ProtoField(1, ScalarType.UINT32), + extStatus: ProtoField(2, ScalarType.UINT32), + batteryStatus: ProtoField(3, ScalarType.UINT32), + customExt: ProtoField(4, () => SetStatusCustomExt, true), +}; + +export const SetStatusCustomExt = { + faceId: ProtoField(1, ScalarType.UINT32), + text: ProtoField(2, ScalarType.STRING, true), + field3: ProtoField(3, ScalarType.UINT32), +}; + +export const SetStatusResponse = { + message: ProtoField(2, ScalarType.STRING), +}; + +export const HttpConn = { + field1: ProtoField(1, ScalarType.INT32), + field2: ProtoField(2, ScalarType.INT32), + field3: ProtoField(3, ScalarType.INT32), + field4: ProtoField(4, ScalarType.INT32), + tgt: ProtoField(5, ScalarType.STRING), + field6: ProtoField(6, ScalarType.INT32), + serviceTypes: ProtoField(7, ScalarType.INT32, false, true), + field9: ProtoField(9, ScalarType.INT32), + field10: ProtoField(10, ScalarType.INT32), + field11: ProtoField(11, ScalarType.INT32), + ver: ProtoField(15, ScalarType.STRING), +}; + +export const HttpConn0x6ff_501 = { + httpConn: ProtoField(0x501, () => HttpConn), +}; + +export const HttpConn0x6ff_501Response = { + httpConn: ProtoField(0x501, () => HttpConnResponse), +}; + +export const HttpConnResponse = { + sigSession: ProtoField(1, ScalarType.BYTES), + sessionKey: ProtoField(2, ScalarType.BYTES), + serverInfos: ProtoField(3, () => ServerInfo, false, true), +}; + +export const ServerAddr = { + type: ProtoField(1, ScalarType.UINT32), + ip: ProtoField(2, ScalarType.FIXED32), + port: ProtoField(3, ScalarType.UINT32), + area: ProtoField(4, ScalarType.UINT32), +}; + +export const ServerInfo = { + serviceType: ProtoField(1, ScalarType.UINT32), + serverAddrs: ProtoField(2, () => ServerAddr, false, true), +}; diff --git a/src/core/packet/proto/highway/highway.ts b/src/core/packet/proto/highway/highway.ts new file mode 100644 index 00000000..cd9cce5d --- /dev/null +++ b/src/core/packet/proto/highway/highway.ts @@ -0,0 +1,155 @@ +import {ScalarType} from "@protobuf-ts/runtime"; +import {ProtoField} from "../NapProto"; +import {MsgInfo, MsgInfoBody} from "@/core/packet/proto/oidb/common/Ntv2.RichMediaReq"; + +export const DataHighwayHead = { + version: ProtoField(1, ScalarType.UINT32), + uin: ProtoField(2, ScalarType.STRING, true), + command: ProtoField(3, ScalarType.STRING, true), + seq: ProtoField(4, ScalarType.UINT32, true), + retryTimes: ProtoField(5, ScalarType.UINT32, true), + appId: ProtoField(6, ScalarType.UINT32), + dataFlag: ProtoField(7, ScalarType.UINT32), + commandId: ProtoField(8, ScalarType.UINT32), + buildVer: ProtoField(9, ScalarType.BYTES, true), +} + +export const FileUploadExt = { + unknown1: ProtoField(1, ScalarType.INT32), + unknown2: ProtoField(2, ScalarType.INT32), + unknown3: ProtoField(3, ScalarType.INT32), + entry: ProtoField(100, () => FileUploadEntry), + unknown200: ProtoField(200, ScalarType.INT32), +} + +export const FileUploadEntry = { + busiBuff: ProtoField(100, () => ExcitingBusiInfo), + fileEntry: ProtoField(200, () => ExcitingFileEntry), + clientInfo: ProtoField(300, () => ExcitingClientInfo), + fileNameInfo: ProtoField(400, () => ExcitingFileNameInfo), + host: ProtoField(500, () => ExcitingHostConfig), +} + +export const ExcitingBusiInfo = { + busId: ProtoField(1, ScalarType.INT32), + senderUin: ProtoField(100, ScalarType.UINT64), + receiverUin: ProtoField(200, ScalarType.UINT64), + groupCode: ProtoField(400, ScalarType.UINT64), +} + +export const ExcitingFileEntry = { + fileSize: ProtoField(100, ScalarType.UINT64), + md5: ProtoField(200, ScalarType.BYTES), + checkKey: ProtoField(300, ScalarType.BYTES), + md5S2: ProtoField(400, ScalarType.BYTES), + fileId: ProtoField(600, ScalarType.STRING), + uploadKey: ProtoField(700, ScalarType.BYTES), +} + +export const ExcitingClientInfo = { + clientType: ProtoField(100, ScalarType.INT32), + appId: ProtoField(200, ScalarType.STRING), + terminalType: ProtoField(300, ScalarType.INT32), + clientVer: ProtoField(400, ScalarType.STRING), + unknown: ProtoField(600, ScalarType.INT32), +} + +export const ExcitingFileNameInfo = { + fileName: ProtoField(100, ScalarType.STRING), +} + +export const ExcitingHostConfig = { + hosts: ProtoField(200, () => ExcitingHostInfo, false, true), +} + +export const ExcitingHostInfo = { + url: ProtoField(1, () => ExcitingUrlInfo), + port: ProtoField(2, ScalarType.UINT32), +} + +export const ExcitingUrlInfo = { + unknown: ProtoField(1, ScalarType.INT32), + host: ProtoField(2, ScalarType.STRING), +} + +export const LoginSigHead = { + uint32LoginSigType: ProtoField(1, ScalarType.UINT32), + bytesLoginSig: ProtoField(2, ScalarType.BYTES), + appId: ProtoField(3, ScalarType.UINT32), +} + +export const NTV2RichMediaHighwayExt = { + fileUuid: ProtoField(1, ScalarType.STRING), + uKey: ProtoField(2, ScalarType.STRING), + network: ProtoField(5, () => NTHighwayNetwork), + msgInfoBody: ProtoField(6, () => MsgInfoBody, false, true), + blockSize: ProtoField(10, ScalarType.UINT32), + hash: ProtoField(11, () => NTHighwayHash), +} + +export const NTHighwayHash = { + fileSha1: ProtoField(1, ScalarType.BYTES, false, true), +} + +export const NTHighwayNetwork = { + ipv4s: ProtoField(1, () => NTHighwayIPv4, false, true), +} + +export const NTHighwayIPv4 = { + domain: ProtoField(1, () => NTHighwayDomain), + port: ProtoField(2, ScalarType.UINT32), +} + +export const NTHighwayDomain = { + isEnable: ProtoField(1, ScalarType.BOOL), + ip: ProtoField(2, ScalarType.STRING), +} + +export const ReqDataHighwayHead = { + msgBaseHead: ProtoField(1, () => DataHighwayHead, true), + msgSegHead: ProtoField(2, () => SegHead, true), + bytesReqExtendInfo: ProtoField(3, ScalarType.BYTES, true), + timestamp: ProtoField(4, ScalarType.UINT64), + msgLoginSigHead: ProtoField(5, () => LoginSigHead, true), +} + +export const RespDataHighwayHead = { + msgBaseHead: ProtoField(1, () => DataHighwayHead, true), + msgSegHead: ProtoField(2, () => SegHead, true), + errorCode: ProtoField(3, ScalarType.UINT32), + allowRetry: ProtoField(4, ScalarType.UINT32), + cacheCost: ProtoField(5, ScalarType.UINT32), + htCost: ProtoField(6, ScalarType.UINT32), + bytesRspExtendInfo: ProtoField(7, ScalarType.BYTES, true), + timestamp: ProtoField(8, ScalarType.UINT64), + range: ProtoField(9, ScalarType.UINT64), + isReset: ProtoField(10, ScalarType.UINT32), +} + +export const SegHead = { + serviceId: ProtoField(1, ScalarType.UINT32, true), + filesize: ProtoField(2, ScalarType.UINT64), + dataOffset: ProtoField(3, ScalarType.UINT64, true), + dataLength: ProtoField(4, ScalarType.UINT32), + retCode: ProtoField(5, ScalarType.UINT32, true), + serviceTicket: ProtoField(6, ScalarType.BYTES), + flag: ProtoField(7, ScalarType.UINT32, true), + md5: ProtoField(8, ScalarType.BYTES), + fileMd5: ProtoField(9, ScalarType.BYTES), + cacheAddr: ProtoField(10, ScalarType.UINT32, true), + queryTimes: ProtoField(11, ScalarType.UINT32), + updateCacheIp: ProtoField(12, ScalarType.UINT32), + cachePort: ProtoField(13, ScalarType.UINT32, true), +} + +export const GroupAvatarExtra = { + type: ProtoField(1, ScalarType.UINT32), + groupUin: ProtoField(2, ScalarType.UINT32), + field3: ProtoField(3, () => GroupAvatarExtraField3), + field5: ProtoField(5, ScalarType.UINT32), + field6: ProtoField(6, ScalarType.UINT32), +} + +export const GroupAvatarExtraField3 = { + field1: ProtoField(1, ScalarType.UINT32), +} diff --git a/src/core/packet/proto/message/action.ts b/src/core/packet/proto/message/action.ts new file mode 100644 index 00000000..29d7945f --- /dev/null +++ b/src/core/packet/proto/message/action.ts @@ -0,0 +1,117 @@ +import { ScalarType } from "@protobuf-ts/runtime"; +import { ProtoField } from "../NapProto"; +import { PushMsgBody } from "@/core/packet/proto/message/message"; + +export const LongMsgResult = { + action: ProtoField(2, () => LongMsgAction) +}; + +export const LongMsgAction = { + actionCommand: ProtoField(1, ScalarType.STRING), + actionData: ProtoField(2, () => LongMsgContent) +}; + +export const LongMsgContent = { + msgBody: ProtoField(1, () => PushMsgBody, false, true) +}; + +export const RecvLongMsgReq = { + info: ProtoField(1, () => RecvLongMsgInfo, true), + settings: ProtoField(15, () => LongMsgSettings, true) +}; + +export const RecvLongMsgInfo = { + uid: ProtoField(1, () => LongMsgUid, true), + resId: ProtoField(2, ScalarType.STRING, true), + acquire: ProtoField(3, ScalarType.BOOL) +}; + +export const LongMsgUid = { + uid: ProtoField(2, ScalarType.STRING, true) +}; + +export const LongMsgSettings = { + field1: ProtoField(1, ScalarType.UINT32), + field2: ProtoField(2, ScalarType.UINT32), + field3: ProtoField(3, ScalarType.UINT32), + field4: ProtoField(4, ScalarType.UINT32) +}; + +export const RecvLongMsgResp = { + result: ProtoField(1, () => RecvLongMsgResult), + settings: ProtoField(15, () => LongMsgSettings) +}; + +export const RecvLongMsgResult = { + resId: ProtoField(3, ScalarType.STRING), + payload: ProtoField(4, ScalarType.BYTES) +}; + +export const SendLongMsgReq = { + info: ProtoField(2, () => SendLongMsgInfo), + settings: ProtoField(15, () => LongMsgSettings) +}; + +export const SendLongMsgInfo = { + type: ProtoField(1, ScalarType.UINT32), + uid: ProtoField(2, () => LongMsgUid, true), + groupUin: ProtoField(3, ScalarType.UINT32, true), + payload: ProtoField(4, ScalarType.BYTES, true) +}; + +export const SendLongMsgResp = { + result: ProtoField(2, () => SendLongMsgResult), + settings: ProtoField(15, () => LongMsgSettings) +}; + +export const SendLongMsgResult = { + resId: ProtoField(3, ScalarType.STRING) +}; + +export const SsoGetGroupMsg = { + info: ProtoField(1, () => SsoGetGroupMsgInfo), + direction: ProtoField(2, ScalarType.BOOL) +}; + +export const SsoGetGroupMsgInfo = { + groupUin: ProtoField(1, ScalarType.UINT32), + startSequence: ProtoField(2, ScalarType.UINT32), + endSequence: ProtoField(3, ScalarType.UINT32) +}; + +export const SsoGetGroupMsgResponse = { + body: ProtoField(3, () => SsoGetGroupMsgResponseBody) +}; + +export const SsoGetGroupMsgResponseBody = { + groupUin: ProtoField(3, ScalarType.UINT32), + startSequence: ProtoField(4, ScalarType.UINT32), + endSequence: ProtoField(5, ScalarType.UINT32), + messages: ProtoField(6, () => PushMsgBody, false, true) +}; + +export const SsoGetRoamMsg = { + friendUid: ProtoField(1, ScalarType.STRING, true), + time: ProtoField(2, ScalarType.UINT32), + random: ProtoField(3, ScalarType.UINT32), + count: ProtoField(4, ScalarType.UINT32), + direction: ProtoField(5, ScalarType.BOOL) +}; + +export const SsoGetRoamMsgResponse = { + friendUid: ProtoField(3, ScalarType.STRING), + timestamp: ProtoField(5, ScalarType.UINT32), + random: ProtoField(6, ScalarType.UINT32), + messages: ProtoField(7, () => PushMsgBody, false, true) +}; + +export const SsoGetC2cMsg = { + friendUid: ProtoField(2, ScalarType.STRING, true), + startSequence: ProtoField(3, ScalarType.UINT32), + endSequence: ProtoField(4, ScalarType.UINT32) +}; + +export const SsoGetC2cMsgResponse = { + friendUid: ProtoField(4, ScalarType.STRING), + messages: ProtoField(7, () => PushMsgBody, false, true) +}; diff --git a/src/core/packet/proto/message/c2c.ts b/src/core/packet/proto/message/c2c.ts new file mode 100644 index 00000000..2a93e5eb --- /dev/null +++ b/src/core/packet/proto/message/c2c.ts @@ -0,0 +1,11 @@ +import { ScalarType } from "@protobuf-ts/runtime"; +import { ProtoField } from "../NapProto"; + +export const C2C = { + uin: ProtoField(1, ScalarType.UINT32, true), + uid: ProtoField(2, ScalarType.STRING, true), + field3: ProtoField(3, ScalarType.UINT32, true), + sig: ProtoField(4, ScalarType.UINT32, true), + receiverUin: ProtoField(5, ScalarType.UINT32, true), + receiverUid: ProtoField(6, ScalarType.STRING, true), +}; diff --git a/src/core/packet/proto/message/component.ts b/src/core/packet/proto/message/component.ts new file mode 100644 index 00000000..984b5f3a --- /dev/null +++ b/src/core/packet/proto/message/component.ts @@ -0,0 +1,147 @@ +import { ScalarType } from "@protobuf-ts/runtime"; +import { ProtoField } from "../NapProto"; +import { Elem } from "@/core/packet/proto/message/element"; + +export const Attr = { + codePage: ProtoField(1, ScalarType.INT32), + time: ProtoField(2, ScalarType.INT32), + random: ProtoField(3, ScalarType.INT32), + color: ProtoField(4, ScalarType.INT32), + size: ProtoField(5, ScalarType.INT32), + effect: ProtoField(6, ScalarType.INT32), + charSet: ProtoField(7, ScalarType.INT32), + pitchAndFamily: ProtoField(8, ScalarType.INT32), + fontName: ProtoField(9, ScalarType.STRING), + reserveData: ProtoField(10, ScalarType.BYTES), +}; + +export const NotOnlineFile = { + fileType: ProtoField(1, ScalarType.INT32, true), + sig: ProtoField(2, ScalarType.BYTES, true), + fileUuid: ProtoField(3, ScalarType.STRING, true), + fileMd5: ProtoField(4, ScalarType.BYTES, true), + fileName: ProtoField(5, ScalarType.STRING, true), + fileSize: ProtoField(6, ScalarType.INT64, true), + note: ProtoField(7, ScalarType.BYTES, true), + reserved: ProtoField(8, ScalarType.INT32, true), + subcmd: ProtoField(9, ScalarType.INT32, true), + microCloud: ProtoField(10, ScalarType.INT32, true), + bytesFileUrls: ProtoField(11, ScalarType.BYTES, false, true), + downloadFlag: ProtoField(12, ScalarType.INT32, true), + dangerEvel: ProtoField(50, ScalarType.INT32, true), + lifeTime: ProtoField(51, ScalarType.INT32, true), + uploadTime: ProtoField(52, ScalarType.INT32, true), + absFileType: ProtoField(53, ScalarType.INT32, true), + clientType: ProtoField(54, ScalarType.INT32, true), + expireTime: ProtoField(55, ScalarType.INT32, true), + pbReserve: ProtoField(56, ScalarType.BYTES, true), + fileHash: ProtoField(57, ScalarType.STRING, true), +}; + +export const Ptt = { + fileType: ProtoField(1, ScalarType.INT32), + srcUin: ProtoField(2, ScalarType.UINT64), + fileUuid: ProtoField(3, ScalarType.STRING), + fileMd5: ProtoField(4, ScalarType.BYTES), + fileName: ProtoField(5, ScalarType.STRING), + fileSize: ProtoField(6, ScalarType.INT32), + reserve: ProtoField(7, ScalarType.BYTES), + fileId: ProtoField(8, ScalarType.INT32), + serverIp: ProtoField(9, ScalarType.INT32), + serverPort: ProtoField(10, ScalarType.INT32), + boolValid: ProtoField(11, ScalarType.BOOL), + signature: ProtoField(12, ScalarType.BYTES), + shortcut: ProtoField(13, ScalarType.BYTES), + fileKey: ProtoField(14, ScalarType.BYTES), + magicPttIndex: ProtoField(15, ScalarType.INT32), + voiceSwitch: ProtoField(16, ScalarType.INT32), + pttUrl: ProtoField(17, ScalarType.BYTES), + groupFileKey: ProtoField(18, ScalarType.STRING), + time: ProtoField(19, ScalarType.INT32), + downPara: ProtoField(20, ScalarType.BYTES), + format: ProtoField(29, ScalarType.INT32), + pbReserve: ProtoField(30, ScalarType.BYTES), + bytesPttUrls: ProtoField(31, ScalarType.BYTES, false, true), + downloadFlag: ProtoField(32, ScalarType.INT32), +}; + +export const RichText = { + attr: ProtoField(1, () => Attr, true), + elems: ProtoField(2, () => Elem, false, true), + notOnlineFile: ProtoField(3, () => NotOnlineFile, true), + ptt: ProtoField(4, () => Ptt, true), +}; + +export const ButtonExtra = { + data: ProtoField(1, () => KeyboardData), +}; + +export const KeyboardData = { + rows: ProtoField(1, () => Row, false, true), +}; + +export const Row = { + buttons: ProtoField(1, () => Button, false, true), +}; + +export const Button = { + id: ProtoField(1, ScalarType.STRING), + renderData: ProtoField(2, () => RenderData), + action: ProtoField(3, () => Action), +}; + +export const RenderData = { + label: ProtoField(1, ScalarType.STRING), + visitedLabel: ProtoField(2, ScalarType.STRING), + style: ProtoField(3, ScalarType.INT32), +}; + +export const Action = { + type: ProtoField(1, ScalarType.INT32), + permission: ProtoField(2, () => Permission), + unsupportTips: ProtoField(4, ScalarType.STRING), + data: ProtoField(5, ScalarType.STRING), + reply: ProtoField(7, ScalarType.BOOL), + enter: ProtoField(8, ScalarType.BOOL), +}; + +export const Permission = { + type: ProtoField(1, ScalarType.INT32), + specifyRoleIds: ProtoField(2, ScalarType.STRING, false, true), + specifyUserIds: ProtoField(3, ScalarType.STRING, false, true), +}; + +export const FileExtra = { + file: ProtoField(1, () => NotOnlineFile), +}; + +export const GroupFileExtra = { + field1: ProtoField(1, ScalarType.UINT32), + fileName: ProtoField(2, ScalarType.STRING), + display: ProtoField(3, ScalarType.STRING), + inner: ProtoField(7, () => GroupFileExtraInner), +}; + +export const GroupFileExtraInner = { + info: ProtoField(2, () => GroupFileExtraInfo), +}; + +export const GroupFileExtraInfo = { + busId: ProtoField(1, ScalarType.UINT32), + fileId: ProtoField(2, ScalarType.STRING), + 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), +}; + +export const ImageExtraUrl = { + origUrl: ProtoField(30, ScalarType.STRING), +}; + +export const PokeExtra = { + type: ProtoField(1, ScalarType.UINT32), + field7: ProtoField(7, ScalarType.UINT32), + field8: ProtoField(8, ScalarType.UINT32), +}; diff --git a/src/core/packet/proto/message/element.ts b/src/core/packet/proto/message/element.ts new file mode 100644 index 00000000..4290870a --- /dev/null +++ b/src/core/packet/proto/message/element.ts @@ -0,0 +1,361 @@ +import {ScalarType} from "@protobuf-ts/runtime"; +import {ProtoField} from "../NapProto"; + +export const Elem = { + text: ProtoField(1, () => Text, true), + face: ProtoField(2, () => Face, true), + onlineImage: ProtoField(3, () => OnlineImage, true), + notOnlineImage: ProtoField(4, () => NotOnlineImage, true), + transElem: ProtoField(5, () => TransElem, true), + marketFace: ProtoField(6, () => MarketFace, true), + customFace: ProtoField(8, () => CustomFace, true), + elemFlags2: ProtoField(9, () => ElemFlags2, true), + richMsg: ProtoField(12, () => RichMsg, true), + groupFile: ProtoField(13, () => GroupFile, true), + extraInfo: ProtoField(16, () => ExtraInfo, true), + videoFile: ProtoField(19, () => VideoFile, true), + anonymousGroupMessage: ProtoField(21, () => AnonymousGroupMessage, true), + customElem: ProtoField(31, () => CustomElem, true), + generalFlags: ProtoField(37, () => GeneralFlags, true), + srcMsg: ProtoField(45, () => SrcMsg, true), + lightAppElem: ProtoField(51, () => LightAppElem, true), + commonElem: ProtoField(53, () => CommonElem, true), +}; + +export const Text = { + str: ProtoField(1, ScalarType.STRING, true), + lint: ProtoField(2, ScalarType.STRING, true), + attr6Buf: ProtoField(3, ScalarType.BYTES, true), + attr7Buf: ProtoField(4, ScalarType.BYTES, true), + buf: ProtoField(11, ScalarType.BYTES, true), + pbReserve: ProtoField(12, ScalarType.BYTES, true), +}; + +export const Face = { + index: ProtoField(1, ScalarType.INT32, true), + old: ProtoField(2, ScalarType.BYTES, true), + buf: ProtoField(11, ScalarType.BYTES, true), +}; + +export const OnlineImage = { + guid: ProtoField(1, ScalarType.BYTES), + filePath: ProtoField(2, ScalarType.BYTES), + oldVerSendFile: ProtoField(3, ScalarType.BYTES), +}; + +export const NotOnlineImage = { + filePath: ProtoField(1, ScalarType.STRING), + fileLen: ProtoField(2, ScalarType.UINT32), + downloadPath: ProtoField(3, ScalarType.STRING), + oldVerSendFile: ProtoField(4, ScalarType.BYTES), + imgType: ProtoField(5, ScalarType.INT32), + previewsImage: ProtoField(6, ScalarType.BYTES), + picMd5: ProtoField(7, ScalarType.BYTES), + picHeight: ProtoField(8, ScalarType.UINT32), + picWidth: ProtoField(9, ScalarType.UINT32), + resId: ProtoField(10, ScalarType.STRING), + flag: ProtoField(11, ScalarType.BYTES), + thumbUrl: ProtoField(12, ScalarType.STRING), + original: ProtoField(13, ScalarType.INT32), + bigUrl: ProtoField(14, ScalarType.STRING), + origUrl: ProtoField(15, ScalarType.STRING), + bizType: ProtoField(16, ScalarType.INT32), + result: ProtoField(17, ScalarType.INT32), + index: ProtoField(18, ScalarType.INT32), + opFaceBuf: ProtoField(19, ScalarType.BYTES), + oldPicMd5: ProtoField(20, ScalarType.BOOL), + thumbWidth: ProtoField(21, ScalarType.INT32), + thumbHeight: ProtoField(22, ScalarType.INT32), + fileId: ProtoField(23, ScalarType.INT32), + showLen: ProtoField(24, ScalarType.UINT32), + downloadLen: ProtoField(25, ScalarType.UINT32), + x400Url: ProtoField(26, ScalarType.STRING), + x400Width: ProtoField(27, ScalarType.INT32), + x400Height: ProtoField(28, ScalarType.INT32), + pbRes: ProtoField(29, () => NotOnlineImage_PbReserve), +}; + +export const NotOnlineImage_PbReserve = { + subType: ProtoField(1, ScalarType.INT32), + field3: ProtoField(3, ScalarType.INT32), + field4: ProtoField(4, ScalarType.INT32), + summary: ProtoField(8, ScalarType.STRING), + field10: ProtoField(10, ScalarType.INT32), + field20: ProtoField(20, () => NotOnlineImage_PbReserve2), + url: ProtoField(30, ScalarType.STRING), + md5Str: ProtoField(31, ScalarType.STRING), +}; + +export const NotOnlineImage_PbReserve2 = { + field1: ProtoField(1, ScalarType.INT32), + field2: ProtoField(2, ScalarType.STRING), + field3: ProtoField(3, ScalarType.INT32), + field4: ProtoField(4, ScalarType.INT32), + field5: ProtoField(5, ScalarType.INT32), + field7: ProtoField(7, ScalarType.STRING), +}; + +export const TransElem = { + elemType: ProtoField(1, ScalarType.INT32), + elemValue: ProtoField(2, ScalarType.BYTES), +}; + +export const MarketFace = { + faceName: ProtoField(1, ScalarType.STRING), + itemType: ProtoField(2, ScalarType.INT32), + faceInfo: ProtoField(3, ScalarType.INT32), + faceId: ProtoField(4, ScalarType.BYTES), + tabId: ProtoField(5, ScalarType.INT32), + subType: ProtoField(6, ScalarType.INT32), + key: ProtoField(7, ScalarType.STRING), + param: ProtoField(8, ScalarType.BYTES), + mediaType: ProtoField(9, ScalarType.INT32), + imageWidth: ProtoField(10, ScalarType.INT32), + imageHeight: ProtoField(11, ScalarType.INT32), + mobileparam: ProtoField(12, ScalarType.BYTES), + pbReserve: ProtoField(13, () => MarketFacePbRes), +}; + +export const MarketFacePbRes = { + field8: ProtoField(8, ScalarType.INT32) +} + +export const CustomFace = { + guid: ProtoField(1, ScalarType.BYTES), + filePath: ProtoField(2, ScalarType.STRING), + shortcut: ProtoField(3, ScalarType.STRING), + buffer: ProtoField(4, ScalarType.BYTES), + flag: ProtoField(5, ScalarType.BYTES), + oldData: ProtoField(6, ScalarType.BYTES, true), + fileId: ProtoField(7, ScalarType.UINT32), + serverIp: ProtoField(8, ScalarType.INT32, true), + serverPort: ProtoField(9, ScalarType.INT32, true), + fileType: ProtoField(10, ScalarType.INT32), + signature: ProtoField(11, ScalarType.BYTES), + useful: ProtoField(12, ScalarType.INT32), + md5: ProtoField(13, ScalarType.BYTES), + thumbUrl: ProtoField(14, ScalarType.STRING), + bigUrl: ProtoField(15, ScalarType.STRING), + origUrl: ProtoField(16, ScalarType.STRING), + bizType: ProtoField(17, ScalarType.INT32), + repeatIndex: ProtoField(18, ScalarType.INT32), + repeatImage: ProtoField(19, ScalarType.INT32), + imageType: ProtoField(20, ScalarType.INT32), + index: ProtoField(21, ScalarType.INT32), + width: ProtoField(22, ScalarType.INT32), + height: ProtoField(23, ScalarType.INT32), + source: ProtoField(24, ScalarType.INT32), + size: ProtoField(25, ScalarType.UINT32), + origin: ProtoField(26, ScalarType.INT32), + thumbWidth: ProtoField(27, ScalarType.INT32, true), + thumbHeight: ProtoField(28, ScalarType.INT32, true), + showLen: ProtoField(29, ScalarType.INT32), + downloadLen: ProtoField(30, ScalarType.INT32), + x400Url: ProtoField(31, ScalarType.STRING, true), + x400Width: ProtoField(32, ScalarType.INT32), + x400Height: ProtoField(33, ScalarType.INT32), + pbRes: ProtoField(34, () => CustomFace_PbReserve, true), +}; + +export const CustomFace_PbReserve = { + subType: ProtoField(1, ScalarType.INT32), + summary: ProtoField(9, ScalarType.STRING), +}; + +export const ElemFlags2 = { + colorTextId: ProtoField(1, ScalarType.UINT32), + msgId: ProtoField(2, ScalarType.UINT64), + whisperSessionId: ProtoField(3, ScalarType.UINT32), + pttChangeBit: ProtoField(4, ScalarType.UINT32), + vipStatus: ProtoField(5, ScalarType.UINT32), + compatibleId: ProtoField(6, ScalarType.UINT32), + insts: ProtoField(7, () => Instance, false, true), + msgRptCnt: ProtoField(8, ScalarType.UINT32), + srcInst: ProtoField(9, () => Instance), + longtitude: ProtoField(10, ScalarType.UINT32), + latitude: ProtoField(11, ScalarType.UINT32), + customFont: ProtoField(12, ScalarType.UINT32), + pcSupportDef: ProtoField(13, () => PcSupportDef), + crmFlags: ProtoField(14, ScalarType.UINT32, true), +}; + +export const PcSupportDef = { + pcPtlBegin: ProtoField(1, ScalarType.UINT32), + pcPtlEnd: ProtoField(2, ScalarType.UINT32), + macPtlBegin: ProtoField(3, ScalarType.UINT32), + macPtlEnd: ProtoField(4, ScalarType.UINT32), + ptlsSupport: ProtoField(5, ScalarType.INT32, false, true), + ptlsNotSupport: ProtoField(6, ScalarType.UINT32, false, true), +}; + +export const Instance = { + appId: ProtoField(1, ScalarType.UINT32), + instId: ProtoField(2, ScalarType.UINT32), +}; + +export const RichMsg = { + template1: ProtoField(1, ScalarType.BYTES, true), + serviceId: ProtoField(2, ScalarType.INT32, true), + msgResId: ProtoField(3, ScalarType.BYTES, true), + rand: ProtoField(4, ScalarType.INT32, true), + seq: ProtoField(5, ScalarType.UINT32, true), +}; + +export const GroupFile = { + filename: ProtoField(1, ScalarType.BYTES), + fileSize: ProtoField(2, ScalarType.UINT64), + fileId: ProtoField(3, ScalarType.BYTES), + batchId: ProtoField(4, ScalarType.BYTES), + fileKey: ProtoField(5, ScalarType.BYTES), + mark: ProtoField(6, ScalarType.BYTES), + sequence: ProtoField(7, ScalarType.UINT64), + batchItemId: ProtoField(8, ScalarType.BYTES), + feedMsgTime: ProtoField(9, ScalarType.INT32), + pbReserve: ProtoField(10, ScalarType.BYTES), +}; + +export const ExtraInfo = { + nick: ProtoField(1, ScalarType.BYTES), + groupCard: ProtoField(2, ScalarType.BYTES), + level: ProtoField(3, ScalarType.INT32), + flags: ProtoField(4, ScalarType.INT32), + groupMask: ProtoField(5, ScalarType.INT32), + msgTailId: ProtoField(6, ScalarType.INT32), + senderTitle: ProtoField(7, ScalarType.BYTES), + apnsTips: ProtoField(8, ScalarType.BYTES), + uin: ProtoField(9, ScalarType.UINT64), + msgStateFlag: ProtoField(10, ScalarType.INT32), + apnsSoundType: ProtoField(11, ScalarType.INT32), + newGroupFlag: ProtoField(12, ScalarType.INT32), +}; + +export const VideoFile = { + fileUuid: ProtoField(1, ScalarType.STRING), + fileMd5: ProtoField(2, ScalarType.BYTES), + fileName: ProtoField(3, ScalarType.STRING), + fileFormat: ProtoField(4, ScalarType.INT32), + fileTime: ProtoField(5, ScalarType.INT32), + fileSize: ProtoField(6, ScalarType.INT32), + thumbWidth: ProtoField(7, ScalarType.INT32), + thumbHeight: ProtoField(8, ScalarType.INT32), + thumbFileMd5: ProtoField(9, ScalarType.BYTES), + source: ProtoField(10, ScalarType.BYTES), + thumbFileSize: ProtoField(11, ScalarType.INT32), + busiType: ProtoField(12, ScalarType.INT32), + fromChatType: ProtoField(13, ScalarType.INT32), + toChatType: ProtoField(14, ScalarType.INT32), + boolSupportProgressive: ProtoField(15, ScalarType.BOOL), + fileWidth: ProtoField(16, ScalarType.INT32), + fileHeight: ProtoField(17, ScalarType.INT32), + subBusiType: ProtoField(18, ScalarType.INT32), + videoAttr: ProtoField(19, ScalarType.INT32), + bytesThumbFileUrls: ProtoField(20, ScalarType.BYTES, false, true), + bytesVideoFileUrls: ProtoField(21, ScalarType.BYTES, false, true), + thumbDownloadFlag: ProtoField(22, ScalarType.INT32), + videoDownloadFlag: ProtoField(23, ScalarType.INT32), + pbReserve: ProtoField(24, ScalarType.BYTES), +}; + +export const AnonymousGroupMessage = { + flags: ProtoField(1, ScalarType.INT32), + anonId: ProtoField(2, ScalarType.BYTES), + anonNick: ProtoField(3, ScalarType.BYTES), + headPortrait: ProtoField(4, ScalarType.INT32), + expireTime: ProtoField(5, ScalarType.INT32), + bubbleId: ProtoField(6, ScalarType.INT32), + rankColor: ProtoField(7, ScalarType.BYTES), +}; + +export const CustomElem = { + desc: ProtoField(1, ScalarType.BYTES), + data: ProtoField(2, ScalarType.BYTES), + enumType: ProtoField(3, ScalarType.INT32), + ext: ProtoField(4, ScalarType.BYTES), + sound: ProtoField(5, ScalarType.BYTES), +}; + +export const GeneralFlags = { + bubbleDiyTextId: ProtoField(1, ScalarType.INT32), + groupFlagNew: ProtoField(2, ScalarType.INT32), + uin: ProtoField(3, ScalarType.UINT64), + rpId: ProtoField(4, ScalarType.BYTES), + prpFold: ProtoField(5, ScalarType.INT32), + longTextFlag: ProtoField(6, ScalarType.INT32), + longTextResId: ProtoField(7, ScalarType.STRING, true), + groupType: ProtoField(8, ScalarType.INT32), + toUinFlag: ProtoField(9, ScalarType.INT32), + glamourLevel: ProtoField(10, ScalarType.INT32), + memberLevel: ProtoField(11, ScalarType.INT32), + groupRankSeq: ProtoField(12, ScalarType.UINT64), + olympicTorch: ProtoField(13, ScalarType.INT32), + babyqGuideMsgCookie: ProtoField(14, ScalarType.BYTES), + uin32ExpertFlag: ProtoField(15, ScalarType.INT32), + bubbleSubId: ProtoField(16, ScalarType.INT32), + pendantId: ProtoField(17, ScalarType.UINT64), + rpIndex: ProtoField(18, ScalarType.BYTES), + pbReserve: ProtoField(19, ScalarType.BYTES), +}; + +export const SrcMsg = { + origSeqs: ProtoField(1, ScalarType.UINT32, false, true), + senderUin: ProtoField(2, ScalarType.UINT64), + time: ProtoField(3, ScalarType.INT32, true), + flag: ProtoField(4, ScalarType.INT32, true), + elems: ProtoField(5, () => Elem, false, true), + type: ProtoField(6, ScalarType.INT32, true), + richMsg: ProtoField(7, ScalarType.BYTES, true), + pbReserve: ProtoField(8, () => SrcMsgPbRes, true), + sourceMsg: ProtoField(9, ScalarType.BYTES, true), + toUin: ProtoField(10, ScalarType.UINT64, true), + troopName: ProtoField(11, ScalarType.BYTES, true), +}; + +export const SrcMsgPbRes = { + messageId: ProtoField(3, ScalarType.UINT64), + senderUid: ProtoField(6, ScalarType.STRING, true), + receiverUid: ProtoField(7, ScalarType.STRING, true), + friendSeq: ProtoField(8, ScalarType.UINT32, true), +} + +export const LightAppElem = { + data: ProtoField(1, ScalarType.BYTES), + msgResid: ProtoField(2, ScalarType.BYTES, true), +}; + +export const CommonElem = { + serviceType: ProtoField(1, ScalarType.INT32), + pbElem: ProtoField(2, ScalarType.BYTES), + businessType: ProtoField(3, ScalarType.UINT32), +}; + +export const FaceExtra = { + faceId: ProtoField(1, ScalarType.INT32, true), +}; + +export const MentionExtra = { + type: ProtoField(3, ScalarType.INT32, true), + uin: ProtoField(4, ScalarType.UINT32, true), + field5: ProtoField(5, ScalarType.INT32, true), + uid: ProtoField(9, ScalarType.STRING, true), +}; + +export const QBigFaceExtra = { + AniStickerPackId: ProtoField(1, ScalarType.STRING, true), + AniStickerId: ProtoField(2, ScalarType.STRING, true), + faceId: ProtoField(3, ScalarType.INT32, true), + Field4: ProtoField(4, ScalarType.INT32, true), + AniStickerType: ProtoField(5, ScalarType.INT32, true), + field6: ProtoField(6, ScalarType.STRING, true), + preview: ProtoField(7, ScalarType.STRING, true), + field9: ProtoField(9, ScalarType.INT32, true), +}; + +export const QSmallFaceExtra = { + faceId: ProtoField(1, ScalarType.UINT32), + preview: ProtoField(2, ScalarType.STRING), + preview2: ProtoField(3, ScalarType.STRING), +}; + +export const MarkdownData = { + content: ProtoField(1, ScalarType.STRING) +} diff --git a/src/core/packet/proto/message/group.ts b/src/core/packet/proto/message/group.ts new file mode 100644 index 00000000..3416f1e5 --- /dev/null +++ b/src/core/packet/proto/message/group.ts @@ -0,0 +1,19 @@ +import { ScalarType } from "@protobuf-ts/runtime"; +import { ProtoField } from "../NapProto"; + +export const GroupRecallMsg = { + type: ProtoField(1, ScalarType.UINT32), + groupUin: ProtoField(2, ScalarType.UINT32), + field3: ProtoField(3, () => GroupRecallMsgField3), + field4: ProtoField(4, () => GroupRecallMsgField4), +}; + +export const GroupRecallMsgField3 = { + sequence: ProtoField(1, ScalarType.UINT32), + random: ProtoField(2, ScalarType.UINT32), + field3: ProtoField(3, ScalarType.UINT32), +}; + +export const GroupRecallMsgField4 = { + field1: ProtoField(1, ScalarType.UINT32), +}; diff --git a/src/core/packet/proto/message/message.ts b/src/core/packet/proto/message/message.ts new file mode 100644 index 00000000..5203f285 --- /dev/null +++ b/src/core/packet/proto/message/message.ts @@ -0,0 +1,75 @@ +import { ScalarType } from "@protobuf-ts/runtime"; +import { ProtoField } from "../NapProto"; +import { ForwardHead, Grp, GrpTmp, ResponseForward, ResponseGrp, Trans0X211, WPATmp } from "@/core/packet/proto/message/routing"; +import { RichText } from "@/core/packet/proto/message/component"; +import { C2C } from "@/core/packet/proto/message/c2c"; + +export const ContentHead = { + type: ProtoField(1, ScalarType.UINT32), + subType: ProtoField(2, ScalarType.UINT32, true), + divSeq: ProtoField(3, ScalarType.UINT32, true), + msgId: ProtoField(4, ScalarType.UINT32, true), + sequence: ProtoField(5, ScalarType.UINT32, true), + timeStamp: ProtoField(6, ScalarType.UINT32, true), + field7: ProtoField(7, ScalarType.UINT64, true), + field8: ProtoField(8, ScalarType.UINT32, true), + field9: ProtoField(9, ScalarType.UINT32, true), + newId: ProtoField(12, ScalarType.UINT64, true), + forward: ProtoField(15, () => ForwardHead, true), +}; + +export const MessageBody = { + richText: ProtoField(1, () => RichText, true), + msgContent: ProtoField(2, ScalarType.BYTES, true), + msgEncryptContent: ProtoField(3, ScalarType.BYTES, true), +}; + +export const Message = { + routingHead: ProtoField(1, () => RoutingHead, true), + contentHead: ProtoField(2, () => ContentHead, true), + body: ProtoField(3, () => MessageBody, true), + clientSequence: ProtoField(4, ScalarType.UINT32, true), + random: ProtoField(5, ScalarType.UINT32, true), + syncCookie: ProtoField(6, ScalarType.BYTES, true), + via: ProtoField(8, ScalarType.UINT32, true), + dataStatist: ProtoField(9, ScalarType.UINT32, true), + ctrl: ProtoField(12, () => MessageControl, true), + multiSendSeq: ProtoField(14, ScalarType.UINT32), +}; + +export const MessageControl = { + msgFlag: ProtoField(1, ScalarType.INT32), +}; + +export const PushMsg = { + message: ProtoField(1, () => PushMsgBody), + status: ProtoField(3, ScalarType.INT32, true), + pingFlag: ProtoField(5, ScalarType.INT32, true), + generalFlag: ProtoField(9, ScalarType.INT32, true), +}; + +export const PushMsgBody = { + responseHead: ProtoField(1, () => ResponseHead), + contentHead: ProtoField(2, () => ContentHead), + body: ProtoField(3, () => MessageBody, true), +}; + +export const ResponseHead = { + fromUin: ProtoField(1, ScalarType.UINT32), + fromUid: ProtoField(2, ScalarType.STRING, true), + type: ProtoField(3, ScalarType.UINT32), + sigMap: ProtoField(4, ScalarType.UINT32), + toUin: ProtoField(5, ScalarType.UINT32), + toUid: ProtoField(6, ScalarType.STRING, true), + forward: ProtoField(7, () => ResponseForward, true), + grp: ProtoField(8, () => ResponseGrp, true), +}; + +export const RoutingHead = { + c2c: ProtoField(1, () => C2C, true), + grp: ProtoField(2, () => Grp, true), + grpTmp: ProtoField(3, () => GrpTmp, true), + wpaTmp: ProtoField(6, () => WPATmp, true), + trans0X211: ProtoField(15, () => Trans0X211, true), +}; + diff --git a/src/core/packet/proto/message/notify.ts b/src/core/packet/proto/message/notify.ts new file mode 100644 index 00000000..e739e151 --- /dev/null +++ b/src/core/packet/proto/message/notify.ts @@ -0,0 +1,22 @@ +import { ScalarType } from "@protobuf-ts/runtime"; +import { ProtoField } from "../NapProto"; + +export const FriendRecall = { + info: ProtoField(1, () => FriendRecallInfo), + instId: ProtoField(2, ScalarType.UINT32), + appId: ProtoField(3, ScalarType.UINT32), + longMessageFlag: ProtoField(4, ScalarType.UINT32), + reserved: ProtoField(5, ScalarType.BYTES), +}; + +export const FriendRecallInfo = { + fromUid: ProtoField(1, ScalarType.STRING), + toUid: ProtoField(2, ScalarType.STRING), + sequence: ProtoField(3, ScalarType.UINT32), + newId: ProtoField(4, ScalarType.UINT64), + time: ProtoField(5, ScalarType.UINT32), + random: ProtoField(6, ScalarType.UINT32), + pkgNum: ProtoField(7, ScalarType.UINT32), + pkgIndex: ProtoField(8, ScalarType.UINT32), + divSeq: ProtoField(9, ScalarType.UINT32), +}; diff --git a/src/core/packet/proto/message/routing.ts b/src/core/packet/proto/message/routing.ts new file mode 100644 index 00000000..7de44012 --- /dev/null +++ b/src/core/packet/proto/message/routing.ts @@ -0,0 +1,41 @@ +import { ScalarType } from "@protobuf-ts/runtime"; +import { ProtoField } from "../NapProto"; + +export const ForwardHead = { + field1: ProtoField(1, ScalarType.UINT32, true), + field2: ProtoField(2, ScalarType.UINT32, true), + field3: ProtoField(3, ScalarType.UINT32, true), + unknownBase64: ProtoField(5, ScalarType.STRING, true), + avatar: ProtoField(6, ScalarType.STRING, true), +}; + +export const Grp = { + groupCode: ProtoField(1, ScalarType.UINT32, true), +}; + +export const GrpTmp = { + groupUin: ProtoField(1, ScalarType.UINT32, true), + toUin: ProtoField(2, ScalarType.UINT32, true), +}; + +export const ResponseForward = { + friendName: ProtoField(6, ScalarType.STRING, true), +}; + +export const ResponseGrp = { + groupUin: ProtoField(1, ScalarType.UINT32), + memberName: ProtoField(4, ScalarType.STRING), + unknown5: ProtoField(5, ScalarType.UINT32), + groupName: ProtoField(7, ScalarType.STRING), +}; + +export const Trans0X211 = { + toUin: ProtoField(1, ScalarType.UINT64, true), + ccCmd: ProtoField(2, ScalarType.UINT32, true), + uid: ProtoField(8, ScalarType.STRING, true), +}; + +export const WPATmp = { + toUin: ProtoField(1, ScalarType.UINT64), + sig: ProtoField(2, ScalarType.BYTES), +}; diff --git a/src/core/packet/proto/oidb/Oidb.0XFE1_2.ts b/src/core/packet/proto/oidb/Oidb.0XFE1_2.ts new file mode 100644 index 00000000..7c6b37be --- /dev/null +++ b/src/core/packet/proto/oidb/Oidb.0XFE1_2.ts @@ -0,0 +1,23 @@ +import { ScalarType } from "@protobuf-ts/runtime"; +import { ProtoField } from "../NapProto"; + +export const OidbSvcTrpcTcp0XFE1_2 = { + uin: ProtoField(1, ScalarType.UINT32), + key: ProtoField(3, () => OidbSvcTrpcTcp0XFE1_2Key, false, true), +}; + +export const OidbSvcTrpcTcp0XFE1_2Key = { + key: ProtoField(1, ScalarType.UINT32) +}; +export const OidbSvcTrpcTcp0XFE1_2RSP_Status = { + key: ProtoField(1, ScalarType.UINT32), + value: ProtoField(2, ScalarType.UINT64) +}; + +export const OidbSvcTrpcTcp0XFE1_2RSP_Data = { + status: ProtoField(2, () => OidbSvcTrpcTcp0XFE1_2RSP_Status) +}; + +export const OidbSvcTrpcTcp0XFE1_2RSP = { + data: ProtoField(1, () => OidbSvcTrpcTcp0XFE1_2RSP_Data) +}; diff --git a/src/core/packet/proto/oidb/Oidb.0x6D6.ts b/src/core/packet/proto/oidb/Oidb.0x6D6.ts new file mode 100644 index 00000000..be16da98 --- /dev/null +++ b/src/core/packet/proto/oidb/Oidb.0x6D6.ts @@ -0,0 +1,100 @@ +import { ScalarType } from "@protobuf-ts/runtime"; +import { ProtoField } from "../NapProto"; + +export const OidbSvcTrpcTcp0x6D6 = { + file: ProtoField(1, () => OidbSvcTrpcTcp0x6D6Upload, true), + download: ProtoField(3, () => OidbSvcTrpcTcp0x6D6Download, true), + delete: ProtoField(4, () => OidbSvcTrpcTcp0x6D6Delete, true), + rename: ProtoField(5, () => OidbSvcTrpcTcp0x6D6Rename, true), + move: ProtoField(6, () => OidbSvcTrpcTcp0x6D6Move, true), +}; + +export const OidbSvcTrpcTcp0x6D6Upload = { + groupUin: ProtoField(1, ScalarType.UINT32), + appId: ProtoField(2, ScalarType.UINT32), + busId: ProtoField(3, ScalarType.UINT32), + entrance: ProtoField(4, ScalarType.UINT32), + targetDirectory: ProtoField(5, ScalarType.STRING), + fileName: ProtoField(6, ScalarType.STRING), + localDirectory: ProtoField(7, ScalarType.STRING), + fileSize: ProtoField(8, ScalarType.UINT64), + fileSha1: ProtoField(9, ScalarType.BYTES), + fileSha3: ProtoField(10, ScalarType.BYTES), + fileMd5: ProtoField(11, ScalarType.BYTES), + field15: ProtoField(15, ScalarType.BOOL), +}; + +export const OidbSvcTrpcTcp0x6D6Download = { + groupUin: ProtoField(1, ScalarType.UINT32), + appId: ProtoField(2, ScalarType.UINT32), + busId: ProtoField(3, ScalarType.UINT32), + fileId: ProtoField(4, ScalarType.STRING), +}; + +export const OidbSvcTrpcTcp0x6D6Delete = { + groupUin: ProtoField(1, ScalarType.UINT32), + busId: ProtoField(3, ScalarType.UINT32), + fileId: ProtoField(5, ScalarType.STRING), +}; + +export const OidbSvcTrpcTcp0x6D6Rename = { + groupUin: ProtoField(1, ScalarType.UINT32), + busId: ProtoField(3, ScalarType.UINT32), + fileId: ProtoField(4, ScalarType.STRING), + parentFolder: ProtoField(5, ScalarType.STRING), + newFileName: ProtoField(6, ScalarType.STRING), +}; + +export const OidbSvcTrpcTcp0x6D6Move = { + groupUin: ProtoField(1, ScalarType.UINT32), + appId: ProtoField(2, ScalarType.UINT32), + busId: ProtoField(3, ScalarType.UINT32), + fileId: ProtoField(4, ScalarType.STRING), + parentDirectory: ProtoField(5, ScalarType.STRING), + targetDirectory: ProtoField(6, ScalarType.STRING), +}; + +export const OidbSvcTrpcTcp0x6D6Response = { + upload: ProtoField(1, () => OidbSvcTrpcTcp0x6D6_0Response), + download: ProtoField(3, () => OidbSvcTrpcTcp0x6D6_2Response), + delete: ProtoField(4, () => OidbSvcTrpcTcp0x6D6_3_4_5Response), + rename: ProtoField(5, () => OidbSvcTrpcTcp0x6D6_3_4_5Response), + move: ProtoField(6, () => OidbSvcTrpcTcp0x6D6_3_4_5Response), +}; + +export const OidbSvcTrpcTcp0x6D6_0Response = { + retCode: ProtoField(1, ScalarType.INT32), + retMsg: ProtoField(2, ScalarType.STRING), + clientWording: ProtoField(3, ScalarType.STRING), + uploadIp: ProtoField(4, ScalarType.STRING), + serverDns: ProtoField(5, ScalarType.STRING), + busId: ProtoField(6, ScalarType.INT32), + fileId: ProtoField(7, ScalarType.STRING), + checkKey: ProtoField(8, ScalarType.BYTES), + fileKey: ProtoField(9, ScalarType.BYTES), + boolFileExist: ProtoField(10, ScalarType.BOOL), + uploadIpLanV4: ProtoField(12, ScalarType.STRING, false, true), + uploadIpLanV6: ProtoField(13, ScalarType.STRING, false, true), + uploadPort: ProtoField(14, ScalarType.UINT32), +}; + +export const OidbSvcTrpcTcp0x6D6_2Response = { + retCode: ProtoField(1, ScalarType.INT32), + retMsg: ProtoField(2, ScalarType.STRING), + clientWording: ProtoField(3, ScalarType.STRING), + downloadIp: ProtoField(4, ScalarType.STRING), + downloadDns: ProtoField(5, ScalarType.STRING), + downloadUrl: ProtoField(6, ScalarType.BYTES), + fileSha1: ProtoField(7, ScalarType.BYTES), + fileSha3: ProtoField(8, ScalarType.BYTES), + fileMd5: ProtoField(9, ScalarType.BYTES), + cookieVal: ProtoField(10, ScalarType.BYTES), + saveFileName: ProtoField(11, ScalarType.STRING), + previewPort: ProtoField(12, ScalarType.UINT32), +}; + +export const OidbSvcTrpcTcp0x6D6_3_4_5Response = { + retCode: ProtoField(1, ScalarType.INT32), + retMsg: ProtoField(2, ScalarType.STRING), + clientWording: ProtoField(3, ScalarType.STRING), +}; diff --git a/src/core/packet/proto/oidb/Oidb.0x8FC_2.ts b/src/core/packet/proto/oidb/Oidb.0x8FC_2.ts new file mode 100644 index 00000000..3721ee8b --- /dev/null +++ b/src/core/packet/proto/oidb/Oidb.0x8FC_2.ts @@ -0,0 +1,16 @@ +import { ScalarType } from "@protobuf-ts/runtime"; +import { ProtoField } from "../NapProto"; + + +//设置群头衔 OidbSvcTrpcTcp.0x8fc_2 +export const OidbSvcTrpcTcp0X8FC_2_Body = { + targetUid: ProtoField(1, ScalarType.STRING), + specialTitle: ProtoField(5, ScalarType.STRING), + expiredTime: ProtoField(6, ScalarType.SINT32), + uinName: ProtoField(7, ScalarType.STRING), + targetName: ProtoField(8, ScalarType.STRING), +}; +export const OidbSvcTrpcTcp0X8FC_2 = { + groupUin: ProtoField(1, ScalarType.UINT32), + body: ProtoField(3, ScalarType.BYTES), +}; \ No newline at end of file diff --git a/src/core/packet/proto/oidb/Oidb.0x9067_202.ts b/src/core/packet/proto/oidb/Oidb.0x9067_202.ts new file mode 100644 index 00000000..ca52561a --- /dev/null +++ b/src/core/packet/proto/oidb/Oidb.0x9067_202.ts @@ -0,0 +1,26 @@ +import { ScalarType } from "@protobuf-ts/runtime"; +import { ProtoField } from "../NapProto"; +import { MultiMediaReqHead } from "./common/Ntv2.RichMediaReq"; + +//Req +export const OidbSvcTrpcTcp0X9067_202 = { + ReqHead: ProtoField(1, () => MultiMediaReqHead), + DownloadRKeyReq: ProtoField(4, () => OidbSvcTrpcTcp0X9067_202Key), +}; +export const OidbSvcTrpcTcp0X9067_202Key = { + key: ProtoField(1, ScalarType.INT32, false, true), +}; + +//Rsp +export const OidbSvcTrpcTcp0X9067_202_RkeyList = { + rkey: ProtoField(1, ScalarType.STRING), + time: ProtoField(4, ScalarType.UINT32), + type: ProtoField(5, ScalarType.UINT32), + +}; +export const OidbSvcTrpcTcp0X9067_202_Data = { + rkeyList: ProtoField(1, () => OidbSvcTrpcTcp0X9067_202_RkeyList, false, true), +}; +export const OidbSvcTrpcTcp0X9067_202_Rsp_Body = { + data: ProtoField(4, () => OidbSvcTrpcTcp0X9067_202_Data), +}; diff --git a/src/core/packet/proto/oidb/Oidb.0xE37_1200.ts b/src/core/packet/proto/oidb/Oidb.0xE37_1200.ts new file mode 100644 index 00000000..80ecbe2c --- /dev/null +++ b/src/core/packet/proto/oidb/Oidb.0xE37_1200.ts @@ -0,0 +1,61 @@ +import { ScalarType } from "@protobuf-ts/runtime"; +import { ProtoField } from "../NapProto"; + +export const OidbSvcTrpcTcp0XE37_1200 = { + subCommand: ProtoField(1, ScalarType.UINT32, true), + field2: ProtoField(2, ScalarType.INT32, true), + body: ProtoField(14, () => OidbSvcTrpcTcp0XE37_1200Body, true), + field101: ProtoField(101, ScalarType.INT32, true), + field102: ProtoField(102, ScalarType.INT32, true), + field200: ProtoField(200, ScalarType.INT32, true), + field99999: ProtoField(99999, ScalarType.BYTES, true), +}; + +export const OidbSvcTrpcTcp0XE37_1200Body = { + receiverUid: ProtoField(10, ScalarType.STRING, true), + fileUuid: ProtoField(20, ScalarType.STRING, true), + type: ProtoField(30, ScalarType.INT32, true), + fileHash: ProtoField(60, ScalarType.STRING, true), + t2: ProtoField(601, ScalarType.INT32, true), +}; + +export const OidbSvcTrpcTcp0XE37_1200Response = { + command: ProtoField(1, ScalarType.UINT32, true), + subCommand: ProtoField(2, ScalarType.UINT32, true), + body: ProtoField(14, () => OidbSvcTrpcTcp0XE37_1200ResponseBody, true), + field50: ProtoField(50, ScalarType.UINT32, true), +}; + +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), +}; + +export const OidbSvcTrpcTcp0XE37_1200Result = { + server: ProtoField(20, ScalarType.STRING, true), + port: ProtoField(40, ScalarType.UINT32, true), + url: ProtoField(50, ScalarType.STRING, true), + additionalServer: ProtoField(60, ScalarType.STRING, false, true), + ssoPort: ProtoField(80, ScalarType.UINT32, true), + ssoUrl: ProtoField(90, ScalarType.STRING, true), + extra: ProtoField(120, ScalarType.BYTES, true), +}; + +export const OidbSvcTrpcTcp0XE37_1200Metadata = { + uin: ProtoField(1, ScalarType.UINT32, true), + field2: ProtoField(2, ScalarType.UINT32, true), + field3: ProtoField(3, ScalarType.UINT32, true), + size: ProtoField(4, ScalarType.UINT32, true), + timestamp: ProtoField(5, ScalarType.UINT32, true), + fileUuid: ProtoField(6, ScalarType.STRING, true), + fileName: ProtoField(7, ScalarType.STRING, true), + field100: ProtoField(100, ScalarType.BYTES, true), + field101: ProtoField(101, ScalarType.BYTES, true), + field110: ProtoField(110, ScalarType.UINT32, true), + timestamp1: ProtoField(130, ScalarType.UINT32, true), + fileHash: ProtoField(140, ScalarType.STRING, true), + field141: ProtoField(141, ScalarType.BYTES, true), + field142: ProtoField(142, ScalarType.BYTES, true), +}; diff --git a/src/core/packet/proto/oidb/Oidb.0xED3_1.ts b/src/core/packet/proto/oidb/Oidb.0xED3_1.ts new file mode 100644 index 00000000..5e644c50 --- /dev/null +++ b/src/core/packet/proto/oidb/Oidb.0xED3_1.ts @@ -0,0 +1,10 @@ +import { ScalarType } from "@protobuf-ts/runtime"; +import { ProtoField } from "../NapProto"; + +// Send Poke +export const OidbSvcTrpcTcp0XED3_1 = { + uin: ProtoField(1, ScalarType.UINT32), + groupUin: ProtoField(2, ScalarType.UINT32), + friendUin: ProtoField(5, ScalarType.UINT32), + ext: ProtoField(6, ScalarType.UINT32, true) +}; diff --git a/src/core/packet/proto/oidb/OidbBase.ts b/src/core/packet/proto/oidb/OidbBase.ts new file mode 100644 index 00000000..86b62b07 --- /dev/null +++ b/src/core/packet/proto/oidb/OidbBase.ts @@ -0,0 +1,13 @@ +import { ScalarType } from "@protobuf-ts/runtime"; +import { ProtoField } from "../NapProto"; + +export const OidbSvcTrpcTcpBase = { + command: ProtoField(1, ScalarType.UINT32), + subCommand: ProtoField(2, ScalarType.UINT32), + body: ProtoField(4, ScalarType.BYTES), + errorMsg: ProtoField(5, ScalarType.STRING, true), + isReserved: ProtoField(12, ScalarType.UINT32) +}; +export const OidbSvcTrpcTcpBaseRsp = { + body: ProtoField(4, ScalarType.BYTES) +}; \ No newline at end of file diff --git a/src/core/packet/proto/oidb/common/Ntv2.RichMediaReq.ts b/src/core/packet/proto/oidb/common/Ntv2.RichMediaReq.ts new file mode 100644 index 00000000..0d768159 --- /dev/null +++ b/src/core/packet/proto/oidb/common/Ntv2.RichMediaReq.ts @@ -0,0 +1,214 @@ +import {ScalarType} from "@protobuf-ts/runtime"; +import {ProtoField} from "../../NapProto"; + +export const NTV2RichMediaReq = { + ReqHead: ProtoField(1, () => MultiMediaReqHead), + Upload: ProtoField(2, () => UploadReq), + Download: ProtoField(3, () => DownloadReq), + DownloadRKey: ProtoField(4, () => DownloadRKeyReq), + Delete: ProtoField(5, () => DeleteReq), + UploadCompleted: ProtoField(6, () => UploadCompletedReq), + MsgInfoAuth: ProtoField(7, () => MsgInfoAuthReq), + UploadKeyRenewal: ProtoField(8, () => UploadKeyRenewalReq), + DownloadSafe: ProtoField(9, () => DownloadSafeReq), + Extension: ProtoField(99, ScalarType.BYTES, true), +}; + +export const MultiMediaReqHead = { + Common: ProtoField(1, () => CommonHead), + Scene: ProtoField(2, () => SceneInfo), + Client: ProtoField(3, () => ClientMeta), +}; + +export const CommonHead = { + RequestId: ProtoField(1, ScalarType.UINT32), + Command: ProtoField(2, ScalarType.UINT32), +}; + +export const SceneInfo = { + RequestType: ProtoField(101, ScalarType.UINT32), + BusinessType: ProtoField(102, ScalarType.UINT32), + SceneType: ProtoField(200, ScalarType.UINT32), + C2C: ProtoField(201, () => C2CUserInfo, true), + Group: ProtoField(202, () => NTGroupInfo, true), +}; + +export const C2CUserInfo = { + AccountType: ProtoField(1, ScalarType.UINT32), + TargetUid: ProtoField(2, ScalarType.STRING), +}; + +export const NTGroupInfo = { + GroupUin: ProtoField(1, ScalarType.UINT32), +}; + +export const ClientMeta = { + AgentType: ProtoField(1, ScalarType.UINT32), +}; + +export const DownloadReq = { + Node: ProtoField(1, () => IndexNode), + Download: ProtoField(2, () => DownloadExt), +}; + +export const IndexNode = { + Info: ProtoField(1, () => FileInfo), + FileUuid: ProtoField(2, ScalarType.STRING), + StoreId: ProtoField(3, ScalarType.UINT32), + UploadTime: ProtoField(4, ScalarType.UINT32), + Ttl: ProtoField(5, ScalarType.UINT32), + SubType: ProtoField(6, ScalarType.UINT32), +}; + +export const FileInfo = { + FileSize: ProtoField(1, ScalarType.UINT32), + FileHash: ProtoField(2, ScalarType.STRING), + FileSha1: ProtoField(3, ScalarType.STRING), + FileName: ProtoField(4, ScalarType.STRING), + Type: ProtoField(5, () => FileType), + Width: ProtoField(6, ScalarType.UINT32), + Height: ProtoField(7, ScalarType.UINT32), + Time: ProtoField(8, ScalarType.UINT32), + Original: ProtoField(9, ScalarType.UINT32), +}; + +export const FileType = { + Type: ProtoField(1, ScalarType.UINT32), + PicFormat: ProtoField(2, ScalarType.UINT32), + VideoFormat: ProtoField(3, ScalarType.UINT32), + VoiceFormat: ProtoField(4, ScalarType.UINT32), +}; + +export const DownloadExt = { + Pic: ProtoField(1, () => PicDownloadExt), + Video: ProtoField(2, () => VideoDownloadExt), + Ptt: ProtoField(3, () => PttDownloadExt), +}; + +export const VideoDownloadExt = { + BusiType: ProtoField(1, ScalarType.UINT32), + SceneType: ProtoField(2, ScalarType.UINT32), + SubBusiType: ProtoField(3, ScalarType.UINT32), +}; + +export const PicDownloadExt = {}; + +export const PttDownloadExt = {}; + +export const DownloadRKeyReq = { + Types: ProtoField(1, ScalarType.INT32, false, true), +}; + +export const DeleteReq = { + Index: ProtoField(1, () => IndexNode, false, true), + NeedRecallMsg: ProtoField(2, ScalarType.BOOL), + MsgSeq: ProtoField(3, ScalarType.UINT64), + MsgRandom: ProtoField(4, ScalarType.UINT64), + MsgTime: ProtoField(5, ScalarType.UINT64), +}; + +export const UploadCompletedReq = { + SrvSendMsg: ProtoField(1, ScalarType.BOOL), + ClientRandomId: ProtoField(2, ScalarType.UINT64), + MsgInfo: ProtoField(3, () => MsgInfo), + ClientSeq: ProtoField(4, ScalarType.UINT32), +}; + +export const MsgInfoAuthReq = { + Msg: ProtoField(1, ScalarType.BYTES), + AuthTime: ProtoField(2, ScalarType.UINT64), +}; + +export const DownloadSafeReq = { + Index: ProtoField(1, () => IndexNode), +}; + +export const UploadKeyRenewalReq = { + OldUKey: ProtoField(1, ScalarType.STRING), + SubType: ProtoField(2, ScalarType.UINT32), +}; + +export const MsgInfo = { + MsgInfoBody: ProtoField(1, () => MsgInfoBody, false, true), + ExtBizInfo: ProtoField(2, () => ExtBizInfo), +}; + +export const MsgInfoBody = { + Index: ProtoField(1, () => IndexNode), + Picture: ProtoField(2, () => PictureInfo), + Video: ProtoField(3, () => VideoInfo), + Audio: ProtoField(4, () => AudioInfo), + FileExist: ProtoField(5, ScalarType.BOOL), + HashSum: ProtoField(6, ScalarType.BYTES), +}; + +export const VideoInfo = {}; + +export const AudioInfo = {}; + +export const PictureInfo = { + UrlPath: ProtoField(1, ScalarType.STRING), + Ext: ProtoField(2, () => PicUrlExtInfo), + Domain: ProtoField(3, ScalarType.STRING), +}; + +export const PicUrlExtInfo = { + OriginalParameter: ProtoField(1, ScalarType.STRING), + BigParameter: ProtoField(2, ScalarType.STRING), + ThumbParameter: ProtoField(3, ScalarType.STRING), +}; + +export const VideoExtInfo = { + VideoCodecFormat: ProtoField(1, ScalarType.UINT32), +} + +export const ExtBizInfo = { + Pic: ProtoField(1, () => PicExtBizInfo), + Video: ProtoField(2, () => VideoExtBizInfo), + Ptt: ProtoField(3, () => PttExtBizInfo), + BusiType: ProtoField(10, ScalarType.UINT32), +}; + +export const PttExtBizInfo = { + SrcUin: ProtoField(1, ScalarType.UINT64), + PttScene: ProtoField(2, ScalarType.UINT32), + PttType: ProtoField(3, ScalarType.UINT32), + ChangeVoice: ProtoField(4, ScalarType.UINT32), + Waveform: ProtoField(5, ScalarType.BYTES), + AutoConvertText: ProtoField(6, ScalarType.UINT32), + BytesReserve: ProtoField(11, ScalarType.BYTES), + BytesPbReserve: ProtoField(12, ScalarType.BYTES), + BytesGeneralFlags: ProtoField(13, ScalarType.BYTES), +}; + +export const VideoExtBizInfo = { + FromScene: ProtoField(1, ScalarType.UINT32), + ToScene: ProtoField(2, ScalarType.UINT32), + BytesPbReserve: ProtoField(3, ScalarType.BYTES), +}; + +export const PicExtBizInfo = { + BizType: ProtoField(1, ScalarType.UINT32), + TextSummary: ProtoField(2, ScalarType.STRING), + BytesPbReserveC2c: ProtoField(11, ScalarType.BYTES), + BytesPbReserveTroop: ProtoField(12, ScalarType.BYTES), + FromScene: ProtoField(1001, ScalarType.UINT32), + ToScene: ProtoField(1002, ScalarType.UINT32), + OldFileId: ProtoField(1003, ScalarType.UINT32), +}; + +export const UploadReq = { + UploadInfo: ProtoField(1, () => UploadInfo, false, true), + TryFastUploadCompleted: ProtoField(2, ScalarType.BOOL), + SrvSendMsg: ProtoField(3, ScalarType.BOOL), + ClientRandomId: ProtoField(4, ScalarType.UINT64), + CompatQMsgSceneType: ProtoField(5, ScalarType.UINT32), + ExtBizInfo: ProtoField(6, () => ExtBizInfo), + ClientSeq: ProtoField(7, ScalarType.UINT32), + NoNeedCompatMsg: ProtoField(8, ScalarType.BOOL), +}; + +export const UploadInfo = { + FileInfo: ProtoField(1, () => FileInfo), + SubFileType: ProtoField(2, ScalarType.UINT32), +}; diff --git a/src/core/packet/proto/oidb/common/Ntv2.RichMediaResp.ts b/src/core/packet/proto/oidb/common/Ntv2.RichMediaResp.ts new file mode 100644 index 00000000..0b85b36e --- /dev/null +++ b/src/core/packet/proto/oidb/common/Ntv2.RichMediaResp.ts @@ -0,0 +1,114 @@ +import {ScalarType} from "@protobuf-ts/runtime"; +import {ProtoField} from "../../NapProto"; +import {CommonHead, MsgInfo, PicUrlExtInfo, VideoExtInfo} from "@/core/packet/proto/oidb/common/Ntv2.RichMediaReq"; + +export const NTV2RichMediaResp = { + respHead: ProtoField(1, () => MultiMediaRespHead), + upload: ProtoField(2, () => UploadResp), + download: ProtoField(3, () => DownloadResp), + downloadRKey: ProtoField(4, () => DownloadRKeyResp), + delete: ProtoField(5, () => DeleteResp), + uploadCompleted: ProtoField(6, () => UploadCompletedResp), + msgInfoAuth: ProtoField(7, () => MsgInfoAuthResp), + uploadKeyRenewal: ProtoField(8, () => UploadKeyRenewalResp), + downloadSafe: ProtoField(9, () => DownloadSafeResp), + extension: ProtoField(99, ScalarType.BYTES, true), +} + +export const MultiMediaRespHead = { + common: ProtoField(1, () => CommonHead), + retCode: ProtoField(2, ScalarType.UINT32), + message: ProtoField(3, ScalarType.STRING), +} + +export const DownloadResp = { + rKeyParam: ProtoField(1, ScalarType.STRING), + rKeyTtlSecond: ProtoField(2, ScalarType.UINT32), + info: ProtoField(3, () => DownloadInfo), + rKeyCreateTime: ProtoField(4, ScalarType.UINT32), +} + +export const DownloadInfo = { + domain: ProtoField(1, ScalarType.STRING), + urlPath: ProtoField(2, ScalarType.STRING), + httpsPort: ProtoField(3, ScalarType.UINT32), + ipv4s: ProtoField(4, () => IPv4, false, true), + ipv6s: ProtoField(5, () => IPv6, false, true), + picUrlExtInfo: ProtoField(6, () => PicUrlExtInfo), + videoExtInfo: ProtoField(7, () => VideoExtInfo), +} + +export const IPv4 = { + outIP: ProtoField(1, ScalarType.UINT32), + outPort: ProtoField(2, ScalarType.UINT32), + inIP: ProtoField(3, ScalarType.UINT32), + inPort: ProtoField(4, ScalarType.UINT32), + ipType: ProtoField(5, ScalarType.UINT32), +} + +export const IPv6 = { + outIP: ProtoField(1, ScalarType.BYTES), + outPort: ProtoField(2, ScalarType.UINT32), + inIP: ProtoField(3, ScalarType.BYTES), + inPort: ProtoField(4, ScalarType.UINT32), + ipType: ProtoField(5, ScalarType.UINT32), +} + +export const UploadResp = { + uKey: ProtoField(1, ScalarType.STRING, true), + uKeyTtlSecond: ProtoField(2, ScalarType.UINT32), + ipv4s: ProtoField(3, () => IPv4, false, true), + ipv6s: ProtoField(4, () => IPv6, false, true), + msgSeq: ProtoField(5, ScalarType.UINT64), + msgInfo: ProtoField(6, () => MsgInfo), + ext: ProtoField(7, () => RichMediaStorageTransInfo, false, true), + compatQMsg: ProtoField(8, ScalarType.BYTES), + subFileInfos: ProtoField(10, () => SubFileInfo, false, true), +} + +export const RichMediaStorageTransInfo = { + subType: ProtoField(1, ScalarType.UINT32), + extType: ProtoField(2, ScalarType.UINT32), + extValue: ProtoField(3, ScalarType.BYTES), +} + +export const SubFileInfo = { + subType: ProtoField(1, ScalarType.UINT32), + uKey: ProtoField(2, ScalarType.STRING), + uKeyTtlSecond: ProtoField(3, ScalarType.UINT32), + ipv4s: ProtoField(4, () => IPv4, false, true), + ipv6s: ProtoField(5, () => IPv6, false, true), +} + +export const DownloadSafeResp = { +} + +export const UploadKeyRenewalResp = { + ukey: ProtoField(1, ScalarType.STRING), + ukeyTtlSec: ProtoField(2, ScalarType.UINT64), +} + +export const MsgInfoAuthResp = { + authCode: ProtoField(1, ScalarType.UINT32), + msg: ProtoField(2, ScalarType.BYTES), + resultTime: ProtoField(3, ScalarType.UINT64), +} + +export const UploadCompletedResp = { + msgSeq: ProtoField(1, ScalarType.UINT64), +} + +export const DeleteResp = { +} + +export const DownloadRKeyResp = { + rKeys: ProtoField(1, () => RKeyInfo, false, true), +} + +export const RKeyInfo = { + rkey: ProtoField(1, ScalarType.STRING), + rkeyTtlSec: ProtoField(2, ScalarType.UINT64), + storeId: ProtoField(3, ScalarType.UINT32), + rkeyCreateTime: ProtoField(4, ScalarType.UINT32, true), + type: ProtoField(5, ScalarType.UINT32, true), +} diff --git a/src/core/proto/Message.ts b/src/core/packet/proto/old/Message.ts similarity index 98% rename from src/core/proto/Message.ts rename to src/core/packet/proto/old/Message.ts index 02a52afe..8eb7d3b4 100644 --- a/src/core/proto/Message.ts +++ b/src/core/packet/proto/old/Message.ts @@ -1,3 +1,4 @@ +// TODO: refactor with NapProto import { MessageType, BinaryReader, ScalarType } from '@protobuf-ts/runtime'; export const BodyInner = new MessageType("BodyInner", [ @@ -45,4 +46,4 @@ export function decodeMessage(buffer: Uint8Array): any { export function decodeRecallGroup(buffer: Uint8Array): any { const reader = new BinaryReader(buffer); return RecallGroup.internalBinaryRead(reader, reader.len, { readUnknownField: true, readerFactory: () => new BinaryReader(buffer) }); -} \ No newline at end of file +} diff --git a/src/core/proto/ProfileLike.ts b/src/core/packet/proto/old/ProfileLike.ts similarity index 98% rename from src/core/proto/ProfileLike.ts rename to src/core/packet/proto/old/ProfileLike.ts index 4cf22ad9..e79f089f 100644 --- a/src/core/proto/ProfileLike.ts +++ b/src/core/packet/proto/old/ProfileLike.ts @@ -1,3 +1,4 @@ +// TODO: refactor with NapProto import { MessageType, BinaryReader, ScalarType, RepeatType } from '@protobuf-ts/runtime'; export const LikeDetail = new MessageType("likeDetail", [ @@ -55,4 +56,4 @@ export function decodeProfileLikeTip(buffer: Uint8Array): any { export function decodeSysMessage(buffer: Uint8Array): any { const reader = new BinaryReader(buffer); return SysMessage.internalBinaryRead(reader, reader.len, { readUnknownField: true, readerFactory: () => new BinaryReader(buffer) }); -} \ No newline at end of file +} diff --git a/src/core/packet/session.ts b/src/core/packet/session.ts new file mode 100644 index 00000000..3b95c422 --- /dev/null +++ b/src/core/packet/session.ts @@ -0,0 +1,18 @@ +import { PacketClient } from "@/core/packet/client"; +import { PacketHighwaySession } from "@/core/packet/highway/session"; +import { LogWrapper } from "@/common/log"; +import {PacketPacker} from "@/core/packet/packer"; + +export class PacketSession { + readonly logger: LogWrapper; + readonly client: PacketClient; + readonly packer: PacketPacker; + readonly highwaySession: PacketHighwaySession; + + constructor(logger: LogWrapper, client: PacketClient) { + this.logger = logger; + this.client = client; + this.packer = new PacketPacker(this.logger, this.client); + this.highwaySession = new PacketHighwaySession(this.logger, this.client, this.packer); + } +} diff --git a/src/core/packet/utils/crypto/hash.ts b/src/core/packet/utils/crypto/hash.ts new file mode 100644 index 00000000..53901e6c --- /dev/null +++ b/src/core/packet/utils/crypto/hash.ts @@ -0,0 +1,16 @@ +// love from https://github.com/LagrangeDev/lagrangejs & https://github.com/takayama-lily/oicq +import * as crypto from 'crypto'; +import * as stream from 'stream'; +import * as fs from 'fs'; + +function sha1Stream(readable: stream.Readable) { + return new Promise((resolve, reject) => { + readable.on('error', reject); + readable.pipe(crypto.createHash('sha1').on('error', reject).on('data', resolve)); + }) as Promise; +} + +export function calculateSha1(filePath: string): Promise { + const readable = fs.createReadStream(filePath); + return sha1Stream(readable); +} diff --git a/src/core/packet/utils/crypto/tea.ts b/src/core/packet/utils/crypto/tea.ts new file mode 100644 index 00000000..0c8c1c99 --- /dev/null +++ b/src/core/packet/utils/crypto/tea.ts @@ -0,0 +1,86 @@ +// love from https://github.com/LagrangeDev/lagrangejs/blob/main/src/core/tea.ts & https://github.com/takayama-lily/oicq/blob/main/lib/core/tea.ts +const BUF7 = Buffer.alloc(7); +const deltas = [ + 0x9e3779b9, 0x3c6ef372, 0xdaa66d2b, 0x78dde6e4, 0x1715609d, 0xb54cda56, 0x5384540f, 0xf1bbcdc8, 0x8ff34781, + 0x2e2ac13a, 0xcc623af3, 0x6a99b4ac, 0x08d12e65, 0xa708a81e, 0x454021d7, 0xe3779b90, +]; + +function _toUInt32(num: number) { + return num >>> 0; +} + +function _encrypt(x: number, y: number, k0: number, k1: number, k2: number, k3: number): [number, number] { + for (let i = 0; i < 16; ++i) { + let aa = ((_toUInt32(((y << 4) >>> 0) + k0) ^ _toUInt32(y + deltas[i])) >>> 0) ^ _toUInt32(~~(y / 32) + k1); + aa >>>= 0; + x = _toUInt32(x + aa); + let bb = ((_toUInt32(((x << 4) >>> 0) + k2) ^ _toUInt32(x + deltas[i])) >>> 0) ^ _toUInt32(~~(x / 32) + k3); + bb >>>= 0; + y = _toUInt32(y + bb); + } + return [x, y]; +} + +export function encrypt(data: Buffer, key: Buffer) { + let n = (6 - data.length) >>> 0; + n = (n % 8) + 2; + const v = Buffer.concat([Buffer.from([(n - 2) | 0xf8]), Buffer.allocUnsafe(n), data, BUF7]); + const k0 = key.readUInt32BE(0); + const k1 = key.readUInt32BE(4); + const k2 = key.readUInt32BE(8); + const k3 = key.readUInt32BE(12); + let r1 = 0, r2 = 0, t1 = 0, t2 = 0; + for (let i = 0; i < v.length; i += 8) { + const a1 = v.readUInt32BE(i); + const a2 = v.readUInt32BE(i + 4); + const b1 = a1 ^ r1; + const b2 = a2 ^ r2; + const [x, y] = _encrypt(b1 >>> 0, b2 >>> 0, k0, k1, k2, k3); + r1 = x ^ t1; + r2 = y ^ t2; + t1 = b1; + t2 = b2; + v.writeInt32BE(r1, i); + v.writeInt32BE(r2, i + 4); + } + return v; +} + +function _decrypt(x: number, y: number, k0: number, k1: number, k2: number, k3: number) { + for (let i = 15; i >= 0; --i) { + const aa = ((_toUInt32(((x << 4) >>> 0) + k2) ^ _toUInt32(x + deltas[i])) >>> 0) ^ _toUInt32(~~(x / 32) + k3); + y = (y - aa) >>> 0; + const bb = ((_toUInt32(((y << 4) >>> 0) + k0) ^ _toUInt32(y + deltas[i])) >>> 0) ^ _toUInt32(~~(y / 32) + k1); + x = (x - bb) >>> 0; + } + return [x, y]; +} + +export function decrypt(encrypted: Buffer, key: Buffer) { + if (encrypted.length % 8) throw ERROR_ENCRYPTED_LENGTH; + const k0 = key.readUInt32BE(0); + const k1 = key.readUInt32BE(4); + const k2 = key.readUInt32BE(8); + const k3 = key.readUInt32BE(12); + let r1 = 0, r2 = 0, t1 = 0, t2 = 0, x = 0, y = 0; + for (let i = 0; i < encrypted.length; i += 8) { + const a1 = encrypted.readUInt32BE(i); + const a2 = encrypted.readUInt32BE(i + 4); + const b1 = a1 ^ x; + const b2 = a2 ^ y; + [x, y] = _decrypt(b1 >>> 0, b2 >>> 0, k0, k1, k2, k3); + r1 = x ^ t1; + r2 = y ^ t2; + t1 = a1; + t2 = a2; + encrypted.writeInt32BE(r1, i); + encrypted.writeInt32BE(r2, i + 4); + } + if (Buffer.compare(encrypted.subarray(encrypted.length - 7), BUF7) !== 0) throw ERROR_ENCRYPTED_ILLEGAL + // if (Buffer.compare(encrypted.slice(encrypted.length - 7), BUF7) !== 0) throw ERROR_ENCRYPTED_ILLEGAL; + return encrypted.subarray((encrypted[0] & 0x07) + 3, encrypted.length - 7); + // return encrypted.slice((encrypted[0] & 0x07) + 3, encrypted.length - 7); +} + +const ERROR_ENCRYPTED_LENGTH = new Error('length of encrypted data must be a multiple of 8'); +const ERROR_ENCRYPTED_ILLEGAL = new Error('encrypted data is illegal'); diff --git a/src/core/proto/ImageFileId.ts b/src/core/proto/ImageFileId.ts deleted file mode 100644 index 25ad0ee7..00000000 --- a/src/core/proto/ImageFileId.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { MessageType, BinaryReader, ScalarType, BinaryWriter } from '@protobuf-ts/runtime'; - -export const FileId = new MessageType("FileId", [ - { no: 2, name: "sha1", kind: "scalar", T: ScalarType.BYTES }, - { no: 4, name: "appid", kind: "scalar", T: ScalarType.UINT32 }, -]); - -export function encodePBFileId(message: any) { - return FileId.internalBinaryWrite(message, new BinaryWriter(), { - writerFactory: () => new BinaryWriter(), - writeUnknownFields: false - }).finish(); -} - -export function decodePBFileId(buffer: Uint8Array): any { - const reader = new BinaryReader(buffer); - return FileId.internalBinaryRead(reader, reader.len, { - readUnknownField: true, - readerFactory: () => new BinaryReader(buffer) - }); -} \ No newline at end of file diff --git a/src/core/proto/Oidb.fe1_2.ts b/src/core/proto/Oidb.fe1_2.ts deleted file mode 100644 index 2af198c6..00000000 --- a/src/core/proto/Oidb.fe1_2.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { MessageType, ScalarType } from "@protobuf-ts/runtime"; -import { OidbSvcTrpcTcpBase } from "./Poke"; - -export const OidbSvcTrpcTcp0XFE1_2 = new MessageType("oidb_svc_trpctcp_0xfe1_2", [ - { no: 1, name: "uin", kind: "scalar", T: ScalarType.UINT32 }, - { no: 3, name: "key", kind: "scalar", T: ScalarType.BYTES, opt: true } -]); -export function encode_packet_0xfe1_2(PeerUin: string) { - let Body = OidbSvcTrpcTcp0XFE1_2.toBinary - ({ - uin: parseInt(PeerUin), - key: new Uint8Array([0x00, 0x00, 0x00, 0x00]) - }); - return OidbSvcTrpcTcpBase.toBinary - ({ - command: 0xfe1, - subcommand: 2, - body: Body, - isreserved: 1 - }); -} \ No newline at end of file diff --git a/src/core/proto/Poke.ts b/src/core/proto/Poke.ts deleted file mode 100644 index 3829d4ef..00000000 --- a/src/core/proto/Poke.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { MessageType, ScalarType, BinaryWriter } from '@protobuf-ts/runtime'; - -export const OidbSvcTrpcTcpBase = new MessageType("oidb_svc_trpctcp_base", [ - { no: 1, name: "command", kind: "scalar", T: ScalarType.UINT32 }, - { no: 2, name: "subcommand", kind: "scalar", T: ScalarType.UINT32, opt: true }, - { no: 4, name: "body", kind: "scalar", T: ScalarType.BYTES, opt: true }, - { no: 12, name: "isreserved", kind: "scalar", T: ScalarType.INT32, opt: true } -]); - -export const OidbSvcTrpcTcp0XED3_1 = new MessageType("oidb_svc_trpctcp_0xed3_1", [ - { no: 1, name: "uin", kind: "scalar", T: ScalarType.UINT32 }, - { no: 2, name: "groupuin", kind: "scalar", T: ScalarType.UINT32, opt: true }, - { no: 5, name: "frienduin", kind: "scalar", T: ScalarType.UINT32, opt: true }, - { no: 6, name: "ext", kind: "scalar", T: ScalarType.UINT32 } -]); - -export function encodeGroupPoke(groupUin: number, PeerUin: number) { - let Body = OidbSvcTrpcTcp0XED3_1.toBinary - ({ - uin: PeerUin, - groupuin: groupUin, - ext: 0 - }); - //console.log(Body) - return OidbSvcTrpcTcpBase.toBinary - ({ - command: 0xed3, - subcommand: 1, - body: Body - }); -} \ No newline at end of file diff --git a/src/core/services/NodeIKernelBuddyService.ts b/src/core/services/NodeIKernelBuddyService.ts index ac96b5a3..882cfcb2 100644 --- a/src/core/services/NodeIKernelBuddyService.ts +++ b/src/core/services/NodeIKernelBuddyService.ts @@ -36,7 +36,7 @@ export interface NodeIKernelBuddyService { getBuddyRemark(uid: number): string; - setBuddyRemark(uid: number, remark: string): void; + setBuddyRemark(uid: string, remark: string): void; getAvatarUrl(uid: number): string; diff --git a/src/core/services/NodeIKernelECDHService.ts b/src/core/services/NodeIKernelECDHService.ts index fd8231a3..bf8af076 100644 --- a/src/core/services/NodeIKernelECDHService.ts +++ b/src/core/services/NodeIKernelECDHService.ts @@ -1,2 +1,3 @@ export interface NodeIKernelECDHService { + sendOIDBECRequest: (data: Uint8Array) => Promise; } diff --git a/src/core/services/NodeIKernelGroupService.ts b/src/core/services/NodeIKernelGroupService.ts index 6dfb0fbf..252db0e8 100644 --- a/src/core/services/NodeIKernelGroupService.ts +++ b/src/core/services/NodeIKernelGroupService.ts @@ -115,7 +115,8 @@ export interface NodeIKernelGroupService { destroyMemberListScene(SceneId: string): void; getNextMemberList(sceneId: string, a: undefined, num: number): Promise<{ - errCode: number, errMsg: string, + errCode: number, + errMsg: string, result: { ids: string[], infos: Map, finish: boolean, hasRobot: boolean } }>; @@ -145,7 +146,7 @@ export interface NodeIKernelGroupService { getMemberExtInfo(param: GroupExtParam): Promise;//req - getGroupAllInfo(): unknown; + getGroupAllInfo(groupId: string, sourceId: number): Promise; getDiscussExistInfo(): unknown; @@ -234,7 +235,7 @@ export interface NodeIKernelGroupService { setGroupShutUp(groupCode: string, shutUp: boolean): void; - getGroupShutUpMemberList(groupCode: string): unknown[]; + getGroupShutUpMemberList(groupCode: string): Promise; setMemberShutUp(groupCode: string, memberTimes: { uid: string, timeStamp: number }[]): Promise; diff --git a/src/core/services/NodeIKernelMSFService.ts b/src/core/services/NodeIKernelMSFService.ts index fe63ed60..c5b975fa 100644 --- a/src/core/services/NodeIKernelMSFService.ts +++ b/src/core/services/NodeIKernelMSFService.ts @@ -1,3 +1,30 @@ +import { GeneralCallResult } from "./common"; + export interface NodeIKernelMSFService { getServerTime(): string; + setNetworkProxy(param: { + userName: string, + userPwd: string, + address: string, + port: number, + proxyType: number, + domain: string, + isSocket: boolean + }): Promise; + //http + // userName: '', + // userPwd: '', + // address: '127.0.0.1', + // port: 5666, + // proxyType: 1, + // domain: '', + // isSocket: false + //socket + // userName: '', + // userPwd: '', + // address: '127.0.0.1', + // port: 5667, + // proxyType: 2, + // domain: '', + // isSocket: true } \ No newline at end of file diff --git a/src/core/services/NodeIKernelProfileService.ts b/src/core/services/NodeIKernelProfileService.ts index b27e8e0d..cfcb18bf 100644 --- a/src/core/services/NodeIKernelProfileService.ts +++ b/src/core/services/NodeIKernelProfileService.ts @@ -45,7 +45,7 @@ export interface NodeIKernelProfileService { setGander(...args: unknown[]): Promise; - setHeader(arg: string): Promise; + setHeader(arg: string): Promise; setRecommendImgFlag(...args: unknown[]): Promise; diff --git a/src/native/index.ts b/src/native/index.ts index 97fed730..6369cd1a 100644 --- a/src/native/index.ts +++ b/src/native/index.ts @@ -14,7 +14,7 @@ export class Native { if (!this.supportedPlatforms.includes(this.platform)) { throw new Error(`Platform ${this.platform} is not supported`); } - let nativeNode = path.join(nodePath, './native/MoeHoo.win32.node'); + const nativeNode = path.join(nodePath, './native/MoeHoo.win32.node'); if (fs.existsSync(nativeNode)) { dlopen(this.MoeHooExport, nativeNode, constants.dlopen.RTLD_LAZY); } diff --git a/src/onebot/action/extends/GetRkey.ts b/src/onebot/action/extends/GetRkey.ts new file mode 100644 index 00000000..8ec95b22 --- /dev/null +++ b/src/onebot/action/extends/GetRkey.ts @@ -0,0 +1,11 @@ +import { ActionName } from '../types'; +import { GetPacketStatusDepends } from "@/onebot/action/packet/GetPacketStatus"; + + +export class GetRkey extends GetPacketStatusDepends> { + actionName = ActionName.GetRkey; + + async _handle() { + return await this.core.apis.PacketApi.sendRkeyPacket(); + } +} diff --git a/src/onebot/action/extends/GetUserStatus.ts b/src/onebot/action/extends/GetUserStatus.ts new file mode 100644 index 00000000..0f1f3353 --- /dev/null +++ b/src/onebot/action/extends/GetUserStatus.ts @@ -0,0 +1,25 @@ +import BaseAction from '../BaseAction'; +import { ActionName } from '../types'; +import { FromSchema, JSONSchema } from 'json-schema-to-ts'; +// no_cache get时传字符串 +const SchemaData = { + type: 'object', + properties: { + user_id: { type: ['number', 'string'] }, + }, + required: ['user_id'], +} as const satisfies JSONSchema; + +type Payload = FromSchema; + +export class GetUserStatus extends BaseAction { + actionName = ActionName.GetUserStatus; + payloadSchema = SchemaData; + + async _handle(payload: Payload) { + if (!this.core.apis.PacketApi?.available) { + throw new Error('PacketClient is not init'); + } + return await this.core.apis.PacketApi.sendStatusPacket(+payload.user_id); + } +} diff --git a/src/onebot/action/extends/SetQQAvatar.ts b/src/onebot/action/extends/SetQQAvatar.ts index c75d82f6..07fb9bae 100644 --- a/src/onebot/action/extends/SetQQAvatar.ts +++ b/src/onebot/action/extends/SetQQAvatar.ts @@ -38,6 +38,7 @@ export default class SetAvatar extends BaseAction { throw `头像${payload.file}设置失败,api无返回`; } // log(`头像设置返回:${JSON.stringify(ret)}`) + // @ts-ignore if (ret['result'] == 1004022) { throw `头像${payload.file}设置失败,文件可能不是图片格式`; } else if (ret['result'] != 0) { diff --git a/src/onebot/action/extends/SetSpecialTittle.ts b/src/onebot/action/extends/SetSpecialTittle.ts new file mode 100644 index 00000000..2c54d501 --- /dev/null +++ b/src/onebot/action/extends/SetSpecialTittle.ts @@ -0,0 +1,25 @@ +import { ActionName } from '../types'; +import { FromSchema, JSONSchema } from 'json-schema-to-ts'; +import { GetPacketStatusDepends } from "@/onebot/action/packet/GetPacketStatus"; +const SchemaData = { + type: 'object', + properties: { + group_id: { type: ['number', 'string'] }, + user_id: { type: ['number', 'string'] }, + special_title: { type: 'string' }, + }, + required: ['group_id', 'user_id', 'special_title'], +} as const satisfies JSONSchema; + +type Payload = FromSchema; + +export class SetSpecialTittle extends GetPacketStatusDepends { + actionName = ActionName.SetSpecialTittle; + payloadSchema = SchemaData; + + async _handle(payload: Payload) { + const uid = await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString()); + if(!uid) throw new Error('User not found'); + await this.core.apis.PacketApi.sendSetSpecialTittlePacket(payload.group_id.toString(), uid, payload.special_title); + } +} diff --git a/src/onebot/action/file/GetGroupFileUrl.ts b/src/onebot/action/file/GetGroupFileUrl.ts new file mode 100644 index 00000000..147df302 --- /dev/null +++ b/src/onebot/action/file/GetGroupFileUrl.ts @@ -0,0 +1,34 @@ +import { ActionName } from '../types'; +import { FromSchema, JSONSchema } from 'json-schema-to-ts'; +import { FileNapCatOneBotUUID } from "@/common/helper"; +import { GetPacketStatusDepends } from "@/onebot/action/packet/GetPacketStatus"; + +const SchemaData = { + type: 'object', + properties: { + group_id: { type: ['number', 'string'] }, + file_id: { type: ['string'] }, + }, + required: ['group_id', 'file_id'], +} as const satisfies JSONSchema; + +type Payload = FromSchema; + +interface GetGroupFileUrlResponse { + url?: string; +} + +export class GetGroupFileUrl extends GetPacketStatusDepends { + actionName = ActionName.GOCQHTTP_GetGroupFileUrl; + payloadSchema = SchemaData; + + async _handle(payload: Payload) { + const contextMsgFile = FileNapCatOneBotUUID.decode(payload.file_id) || FileNapCatOneBotUUID.decodeModelId(payload.file_id); + if (contextMsgFile?.fileUUID) { + return { + url: await this.core.apis.PacketApi.sendGroupFileDownloadReq(+payload.group_id, contextMsgFile.fileUUID) + } + } + throw new Error('real fileUUID not found!'); + } +} diff --git a/src/onebot/action/file/GetRecord.ts b/src/onebot/action/file/GetRecord.ts index 933ba784..742a8b50 100644 --- a/src/onebot/action/file/GetRecord.ts +++ b/src/onebot/action/file/GetRecord.ts @@ -1,5 +1,9 @@ import { GetFileBase, GetFilePayload, GetFileResponse } from './GetFile'; import { ActionName } from '../types'; +import { spawn } from 'node:child_process'; +import { promises as fs } from 'fs'; +import { decode } from 'silk-wasm'; +const FFMPEG_PATH = process.env.FFMPEG_PATH || 'ffmpeg'; interface Payload extends GetFilePayload { out_format: 'mp3' | 'amr' | 'wma' | 'm4a' | 'spx' | 'ogg' | 'wav' | 'flac'; @@ -9,7 +13,54 @@ export default class GetRecord extends GetFileBase { actionName = ActionName.GetRecord; async _handle(payload: Payload): Promise { - const res = super._handle(payload); + const res = await super._handle(payload); + if (payload.out_format && typeof payload.out_format === 'string') { + const inputFile = res.file; + if (!inputFile) throw new Error('file not found'); + const pcmFile = `${inputFile}.pcm`; + const outputFile = `${inputFile}.${payload.out_format}`; + try { + await fs.access(inputFile); + await this.decodeFile(inputFile, pcmFile); + await this.convertFile(pcmFile, outputFile, payload.out_format); + const base64Data = await fs.readFile(outputFile, { encoding: 'base64' }); + res.file = outputFile; + res.url = outputFile; + res.base64 = base64Data; + } catch (error) { + console.error('Error processing file:', error); + throw error; // 重新抛出错误以便调用者可以处理 + } + } return res; } -} + + private async decodeFile(inputFile: string, outputFile: string): Promise { + try { + const inputData = await fs.readFile(inputFile); + const decodedData = await decode(inputData, 24000); + await fs.writeFile(outputFile, Buffer.from(decodedData.data)); + } catch (error) { + console.error('Error decoding file:', error); + throw error; // 重新抛出错误以便调用者可以处理 + } + } + + private convertFile(inputFile: string, outputFile: string, format: string): Promise { + return new Promise((resolve, reject) => { + const ffmpeg = spawn(FFMPEG_PATH, ['-f', 's16le', '-ar', '24000', '-ac', '1', '-i', inputFile, outputFile]); + + ffmpeg.on('close', (code) => { + if (code === 0) { + resolve(); + } else { + reject(new Error(`ffmpeg process exited with code ${code}`)); + } + }); + + ffmpeg.on('error', (error: Error) => { + reject(error); + }); + }); + } +} \ No newline at end of file diff --git a/src/onebot/action/go-cqhttp/GetForwardMsg.ts b/src/onebot/action/go-cqhttp/GetForwardMsg.ts index 6be34fd7..6c6d4890 100644 --- a/src/onebot/action/go-cqhttp/GetForwardMsg.ts +++ b/src/onebot/action/go-cqhttp/GetForwardMsg.ts @@ -1,15 +1,10 @@ import BaseAction from '../BaseAction'; -import { OB11Message, OB11MessageData, OB11MessageDataType, OB11MessageForward, OB11MessageNode as OriginalOB11MessageNode } from '@/onebot'; +import { OB11Message, OB11MessageData, OB11MessageDataType, OB11MessageForward, OB11MessageNodePlain as OB11MessageNode} from '@/onebot'; import { ActionName } from '../types'; import { FromSchema, JSONSchema } from 'json-schema-to-ts'; import { MessageUnique } from '@/common/message-unique'; -type OB11MessageNode = OriginalOB11MessageNode & { - data: { - content?: Array; - message: Array; - }; -}; + const SchemaData = { type: 'object', @@ -83,7 +78,7 @@ export class GoCQHTTPGetForwardMsgAction extends BaseAction { } //if (this.obContext.configLoader.configData.messagePostFormat === 'array') { //提取 - let realmsg = ((await this.parseForward([resMsg]))[0].data.message as OB11MessageNode[])[0].data.message; + const realmsg = ((await this.parseForward([resMsg]))[0].data.message as OB11MessageNode[])[0].data.message; //里面都是offline消息 id都是0 没得说话 return { message: realmsg }; //} diff --git a/src/onebot/action/group/GetGroupMemberInfo.ts b/src/onebot/action/group/GetGroupMemberInfo.ts index 6c1a04fd..82f734ef 100644 --- a/src/onebot/action/group/GetGroupMemberInfo.ts +++ b/src/onebot/action/group/GetGroupMemberInfo.ts @@ -21,28 +21,31 @@ class GetGroupMemberInfo extends BaseAction { actionName = ActionName.GetGroupMemberInfo; payloadSchema = SchemaData; + private parseBoolean(value: boolean | string): boolean { + return typeof value === 'string' ? value === 'true' : value; + } + + private async getUid(userId: string | number): Promise { + const uid = await this.core.apis.UserApi.getUidByUinV2(userId.toString()); + if (!uid) throw new Error(`Uin2Uid Error: 用户ID ${userId} 不存在`); + return uid; + } + async _handle(payload: Payload) { - const isNocache = typeof payload.no_cache === 'string' ? payload.no_cache === 'true' : !!payload.no_cache; - const uid = await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString()); - if (!uid) throw new Error(`Uin2Uid Error ${payload.user_id}不存在`); - const [member, info] = await Promise.allSettled([ + const isNocache = this.parseBoolean(payload.no_cache ?? true); + const uid = await this.getUid(payload.user_id); + const [member, info] = await Promise.all([ this.core.apis.GroupApi.getGroupMemberEx(payload.group_id.toString(), uid, isNocache), this.core.apis.UserApi.getUserDetailInfo(uid), ]); - if (member.status !== 'fulfilled') throw new Error(`群(${payload.group_id})成员${payload.user_id}获取失败 ${member.reason}`); - if (!member.value) throw new Error(`群(${payload.group_id})成员${payload.user_id}不存在`); - if (info.status === 'fulfilled') { - Object.assign(member.value, info.value); + if (!member) throw new Error(`群(${payload.group_id})成员${payload.user_id}不存在`); + if (info) { + Object.assign(member, info); } else { - this.core.context.logger.logDebug(`获取群成员详细信息失败, 只能返回基础信息 ${info.reason}`); + this.core.context.logger.logDebug(`获取群成员详细信息失败, 只能返回基础信息`); } - const date = Math.round(Date.now() / 1000); - const retMember = OB11Entities.groupMember(payload.group_id.toString(), member.value as GroupMember); - const Member = await this.core.apis.GroupApi.getGroupMember(payload.group_id.toString(), retMember.user_id); - retMember.last_sent_time = parseInt(Member?.lastSpeakTime ?? date.toString()); - retMember.join_time = parseInt(Member?.joinTime ?? date.toString()); - return retMember; + return OB11Entities.groupMember(payload.group_id.toString(), member as GroupMember); } } -export default GetGroupMemberInfo; +export default GetGroupMemberInfo; \ No newline at end of file diff --git a/src/onebot/action/group/GetGroupMemberList.ts b/src/onebot/action/group/GetGroupMemberList.ts index df632944..56ec9cc4 100644 --- a/src/onebot/action/group/GetGroupMemberList.ts +++ b/src/onebot/action/group/GetGroupMemberList.ts @@ -3,7 +3,6 @@ import { OB11Entities } from '@/onebot/entities'; import BaseAction from '../BaseAction'; import { ActionName } from '../types'; import { FromSchema, JSONSchema } from 'json-schema-to-ts'; -import { calcQQLevel } from '@/common/helper'; const SchemaData = { type: 'object', @@ -16,61 +15,19 @@ const SchemaData = { type Payload = FromSchema; -class GetGroupMemberList extends BaseAction { +export class GetGroupMemberList extends BaseAction { actionName = ActionName.GetGroupMemberList; payloadSchema = SchemaData; async _handle(payload: Payload) { - const groupMembers = await this.core.apis.GroupApi.getGroupMembersV2(payload.group_id.toString()); - const groupMembersArr = Array.from(groupMembers.values()); - const uids = groupMembersArr.map(item => item.uid); - //let CoreAndBase = await this.core.apis.GroupApi.getCoreAndBaseInfo(uids) - let _groupMembers = groupMembersArr.map(item => { - return OB11Entities.groupMember(payload.group_id.toString(), item); - }); + const groupIdStr = payload.group_id.toString(); + const groupMembers = await this.core.apis.GroupApi.getGroupMembersV2(groupIdStr); - const MemberMap: Map = new Map(); - const date = Math.round(Date.now() / 1000); - - for (let i = 0, len = _groupMembers.length; i < len; i++) { - // 保证基础数据有这个 同时避免群管插件过于依赖这个杀了 - const Member = await this.core.apis.GroupApi.getGroupMember(payload.group_id.toString(), _groupMembers[i].user_id); - _groupMembers[i].join_time = +(Member?.joinTime ?? date); - _groupMembers[i].last_sent_time = +(Member?.lastSpeakTime ?? date); - MemberMap.set(_groupMembers[i].user_id, _groupMembers[i]); - } - - - const selfRole = groupMembers.get(this.core.selfInfo.uid)?.role; - const isPrivilege = selfRole === 3 || selfRole === 4; - - - if (isPrivilege) { - try { - const webGroupMembers = await this.core.apis.WebApi.getGroupMembers(payload.group_id.toString()); - for (let i = 0, len = webGroupMembers.length; i < len; i++) { - if (!webGroupMembers[i]?.uin) { - continue; - } - const MemberData = MemberMap.get(webGroupMembers[i]?.uin); - if (MemberData) { - MemberData.join_time = webGroupMembers[i]?.join_time; - MemberData.last_sent_time = webGroupMembers[i]?.last_speak_time; - MemberData.qage = webGroupMembers[i]?.qage; - MemberData.level = webGroupMembers[i]?.lv.level.toString(); - MemberMap.set(webGroupMembers[i]?.uin, MemberData); - } - } - } catch (e) { - const logger = this.core.context.logger; - logger.logError.bind(logger)('GetGroupMemberList', e); - } - - } - - _groupMembers = Array.from(MemberMap.values()); - return _groupMembers; + const memberPromises = Array.from(groupMembers.values()).map(item => + OB11Entities.groupMember(groupIdStr, item) + ); + const _groupMembers = await Promise.all(memberPromises); + const MemberMap = new Map(_groupMembers.map(member => [member.user_id, member])); + return Array.from(MemberMap.values()); } -} - -export default GetGroupMemberList; +} \ No newline at end of file diff --git a/src/onebot/action/group/GetGroupShutList.ts b/src/onebot/action/group/GetGroupShutList.ts new file mode 100644 index 00000000..94fe7d9b --- /dev/null +++ b/src/onebot/action/group/GetGroupShutList.ts @@ -0,0 +1,24 @@ +import { OB11Group } from '@/onebot'; +import BaseAction from '../BaseAction'; +import { ActionName } from '../types'; +import { FromSchema, JSONSchema } from 'json-schema-to-ts'; + +const SchemaData = { + type: 'object', + properties: { + group_id: { type: ['number', 'string'] }, + }, + required: ['group_id'], +} as const satisfies JSONSchema; + +type Payload = FromSchema; + +export class GetGroupShutList extends BaseAction { + actionName = ActionName.GetGroupShutList; + payloadSchema = SchemaData; + + async _handle(payload: Payload) { + return await this.core.apis.GroupApi.getGroupShutUpMemberList(payload.group_id.toString()); + } +} + diff --git a/src/onebot/action/group/GroupPoke.ts b/src/onebot/action/group/GroupPoke.ts index 6f960776..7c1f0b80 100644 --- a/src/onebot/action/group/GroupPoke.ts +++ b/src/onebot/action/group/GroupPoke.ts @@ -1,6 +1,6 @@ -import BaseAction from '../BaseAction'; import { ActionName } from '../types'; import { FromSchema, JSONSchema } from 'json-schema-to-ts'; +import {GetPacketStatusDepends} from "@/onebot/action/packet/GetPacketStatus"; // no_cache get时传字符串 const SchemaData = { type: 'object', @@ -13,14 +13,11 @@ const SchemaData = { type Payload = FromSchema; -export class GroupPoke extends BaseAction { +export class GroupPoke extends GetPacketStatusDepends { actionName = ActionName.GroupPoke; payloadSchema = SchemaData; async _handle(payload: Payload) { - if (!this.core.apis.PacketApi.PacketClient?.isConnected) { - throw new Error('PacketClient is not init'); - } - this.core.apis.GroupApi.sendPacketPoke(+payload.group_id, +payload.user_id); + await this.core.apis.PacketApi.sendPokePacket(+payload.group_id, +payload.user_id); } -} \ No newline at end of file +} diff --git a/src/onebot/action/index.ts b/src/onebot/action/index.ts index 6bb72665..aa361e56 100644 --- a/src/onebot/action/index.ts +++ b/src/onebot/action/index.ts @@ -3,7 +3,6 @@ import GetLoginInfo from './system/GetLoginInfo'; import GetFriendList from './user/GetFriendList'; import GetGroupList from './group/GetGroupList'; import GetGroupInfo from './group/GetGroupInfo'; -import GetGroupMemberList from './group/GetGroupMemberList'; import GetGroupMemberInfo from './group/GetGroupMemberInfo'; import SendGroupMsg from './group/SendGroupMsg'; import SendPrivateMsg from './msg/SendPrivateMsg'; @@ -85,6 +84,13 @@ import { GetGroupRootFiles } from '@/onebot/action/go-cqhttp/GetGroupRootFiles'; import { GetGroupFilesByFolder } from '@/onebot/action/go-cqhttp/GetGroupFilesByFolder'; import { GetGroupSystemMsg } from './system/GetSystemMsg'; import { GroupPoke } from './group/GroupPoke'; +import { GetUserStatus } from './extends/GetUserStatus'; +import { GetRkey } from './extends/GetRkey'; +import { SetSpecialTittle } from './extends/SetSpecialTittle'; +import { GetGroupShutList } from './group/GetGroupShutList'; +import { GetGroupMemberList } from './group/GetGroupMemberList'; +import { GetGroupFileUrl } from "@/onebot/action/file/GetGroupFileUrl"; +import {GetPacketStatus} from "@/onebot/action/packet/GetPacketStatus"; export type ActionMap = Map>; @@ -181,7 +187,14 @@ export function createActionMap(obContext: NapCatOneBot11Adapter, core: NapCatCo new GetGroupFilesByFolder(obContext, core), new GetGroupSystemMsg(obContext, core), new FetchUserProfileLike(obContext, core), + new GetPacketStatus(obContext, core), new GroupPoke(obContext, core), + new GetUserStatus(obContext, core), + new GetRkey(obContext, core), + new SetSpecialTittle(obContext, core), + // new UploadForwardMsg(obContext, core), + new GetGroupShutList(obContext, core), + new GetGroupFileUrl(obContext, core), ]; const actionMap = new Map(); for (const action of actionHandlers) { diff --git a/src/onebot/action/msg/GetMsg.ts b/src/onebot/action/msg/GetMsg.ts index 9b183db8..1764e465 100644 --- a/src/onebot/action/msg/GetMsg.ts +++ b/src/onebot/action/msg/GetMsg.ts @@ -33,7 +33,7 @@ class GetMsg extends BaseAction { throw new Error('消息不存在'); } const peer = { guildId: '', peerUid: msgIdWithPeer?.Peer.peerUid, chatType: msgIdWithPeer.Peer.chatType }; - let orimsg = this.obContext.recallMsgCache.get(msgIdWithPeer.MsgId); + const orimsg = this.obContext.recallMsgCache.get(msgIdWithPeer.MsgId); let msg: RawMessage; if (orimsg) { msg = orimsg; diff --git a/src/onebot/action/msg/SendMsg.ts b/src/onebot/action/msg/SendMsg.ts index d65d2032..0005b1a8 100644 --- a/src/onebot/action/msg/SendMsg.ts +++ b/src/onebot/action/msg/SendMsg.ts @@ -6,14 +6,18 @@ import { OB11PostContext, OB11PostSendMsg, } from '@/onebot/types'; -import { ActionName, BaseCheckResult } from '@/onebot/action/types'; -import { decodeCQCode } from '@/onebot/cqcode'; -import { MessageUnique } from '@/common/message-unique'; -import { ChatType, ElementType, NapCatCore, Peer, RawMessage, SendMessageElement } from '@/core'; +import {ActionName, BaseCheckResult} from '@/onebot/action/types'; +import {decodeCQCode} from '@/onebot/cqcode'; +import {MessageUnique} from '@/common/message-unique'; +import {ChatType, ElementType, NapCatCore, Peer, RawMessage, SendArkElement, SendMessageElement} from '@/core'; import BaseAction from '../BaseAction'; +import {rawMsgWithSendMsg} from "@/core/packet/msg/converter"; +import {PacketMsg} from "@/core/packet/msg/message"; +import {PacketMultiMsgElement} from "@/core/packet/msg/element"; export interface ReturnDataType { message_id: number; + res_id?: string; } export enum ContextMode { @@ -26,7 +30,7 @@ export enum ContextMode { export function normalize(message: OB11MessageMixType, autoEscape = false): OB11MessageData[] { return typeof message === 'string' ? ( autoEscape ? - [{ type: OB11MessageDataType.text, data: { text: message } }] : + [{type: OB11MessageDataType.text, data: {text: message}}] : decodeCQCode(message) ) : Array.isArray(message) ? message : [message]; } @@ -69,7 +73,7 @@ export async function createContext(core: NapCatCore, payload: OB11PostContext, } return { chatType: ChatType.KCHATTYPEC2C, - peerUid: Uid!, + peerUid: Uid, guildId: '', }; } @@ -96,10 +100,11 @@ export class SendMsg extends BaseAction { message: '转发消息不能和普通消息混在一起发送,转发需要保证message只有type为node的元素', }; } - return { valid: true }; + return {valid: true}; } - async _handle(payload: OB11PostSendMsg): Promise<{ message_id: number }> { + async _handle(payload: OB11PostSendMsg): Promise { + this.contextMode = ContextMode.Normal; if (payload.message_type === 'group') this.contextMode = ContextMode.Group; if (payload.message_type === 'private') this.contextMode = ContextMode.Private; const peer = await createContext(this.core, payload, this.contextMode); @@ -110,17 +115,19 @@ export class SendMsg extends BaseAction { ); if (getSpecialMsgNum(payload, OB11MessageDataType.node)) { - const returnMsg = await this.handleForwardedNodes(peer, messages as OB11MessageNode[]); - if (returnMsg) { + const packetMode = this.core.apis.PacketApi.available + const returnMsgAndResId = packetMode + ? await this.handleForwardedNodesPacket(peer, messages as OB11MessageNode[]) + : await this.handleForwardedNodes(peer, messages as OB11MessageNode[]); + if (returnMsgAndResId.message) { const msgShortId = MessageUnique.createUniqueMsgId({ guildId: '', peerUid: peer.peerUid, chatType: peer.chatType, - }, returnMsg!.msgId); - return { message_id: msgShortId! }; - } else { - throw Error('发送转发消息失败'); + }, (returnMsgAndResId.message)!.msgId); + return {message_id: msgShortId!, res_id: returnMsgAndResId.res_id}; } + throw Error('发送转发消息失败'); } else { // if (getSpecialMsgNum(payload, OB11MessageDataType.music)) { // const music: OB11MessageCustomMusic = messages[0] as OB11MessageCustomMusic; @@ -130,13 +137,61 @@ export class SendMsg extends BaseAction { } // log("send msg:", peer, sendElements) - const { sendElements, deleteAfterSentFiles } = await this.obContext.apis.MsgApi + const {sendElements, deleteAfterSentFiles} = await this.obContext.apis.MsgApi .createSendElements(messages, peer); const returnMsg = await this.obContext.apis.MsgApi.sendMsgWithOb11UniqueId(peer, sendElements, deleteAfterSentFiles); - return { message_id: returnMsg!.id! }; + return {message_id: returnMsg!.id!}; } - private async handleForwardedNodes(destPeer: Peer, messageNodes: OB11MessageNode[]): Promise { + private async handleForwardedNodesPacket(msgPeer: Peer, messageNodes: OB11MessageNode[]): Promise<{ + message: RawMessage | null, + res_id?: string + }> { + const logger = this.core.context.logger; + const packetMsg: PacketMsg[] = []; + for (const node of messageNodes) { + if ((node.data.id && typeof node.data.content !== "string") || !node.data.id) { + 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, + senderName: node.data.nickname, + groupId: msgPeer.chatType === ChatType.KCHATTYPEGROUP ? +msgPeer.peerUid : undefined, + time: Date.now(), + msg: sendElements, + } + logger.logDebug(`handleForwardedNodesPacket 开始转换 ${JSON.stringify(packetMsgElements)}`); + const transformedMsg = this.core.apis.PacketApi.packetSession?.packer.packetConverter.rawMsgWithSendMsgToPacketMsg(packetMsgElements); + logger.logDebug(`handleForwardedNodesPacket 转换为 ${JSON.stringify(transformedMsg)}`); + packetMsg.push(transformedMsg!); + } else { + logger.logDebug(`handleForwardedNodesPacket 跳过元素 ${JSON.stringify(node)}`); + } + } + const resid = await this.core.apis.PacketApi.sendUploadForwardMsg(packetMsg, msgPeer.chatType === ChatType.KCHATTYPEGROUP ? +msgPeer.peerUid : 0); + const forwardJson = new PacketMultiMsgElement({ + elementType: ElementType.STRUCTLONGMSG, + elementId: "", + structLongMsgElement: { + xmlContent: "", + resId: resid + } + }, packetMsg).JSON; + const finallySendElements = { + elementType: ElementType.ARK, + elementId: "", + arkElement: { + bytesData: JSON.stringify(forwardJson), + }, + } as SendArkElement + const returnMsg = await this.obContext.apis.MsgApi.sendMsgWithOb11UniqueId(msgPeer, [finallySendElements], [], true).catch(_ => undefined) + return {message: returnMsg ?? null, res_id: resid}; + } + + private async handleForwardedNodes(destPeer: Peer, messageNodes: OB11MessageNode[]): Promise<{ + message: RawMessage | null, + res_id?: string + }> { const selfPeer = { chatType: ChatType.KCHATTYPEC2C, peerUid: this.core.selfInfo.uid, @@ -146,7 +201,7 @@ export class SendMsg extends BaseAction { for (const messageNode of messageNodes) { const nodeId = messageNode.data.id; if (nodeId) { - //对Mgsid和OB11ID混用情况兜底 + // 对Msgid和OB11ID混用情况兜底 const nodeMsg = MessageUnique.getMsgIdAndPeerByShortId(parseInt(nodeId)) || MessageUnique.getPeerByMsgId(nodeId); if (!nodeMsg) { logger.logError.bind(this.core.context.logger)('转发消息失败,未找到消息', nodeId); @@ -166,15 +221,15 @@ export class SendMsg extends BaseAction { } const nodeMsg = await this.handleForwardedNodes(selfPeer, OB11Data.filter(e => e.type === OB11MessageDataType.node)); if (nodeMsg) { - nodeMsgIds.push(nodeMsg.msgId); - MessageUnique.createUniqueMsgId(selfPeer, nodeMsg.msgId); + nodeMsgIds.push(nodeMsg.message!.msgId); + MessageUnique.createUniqueMsgId(selfPeer, nodeMsg.message!.msgId); } //完成子卡片生成跳过后续 continue; } - const { sendElements } = await this.obContext.apis.MsgApi + const {sendElements} = await this.obContext.apis.MsgApi .createSendElements(OB11Data, destPeer); - + //拆分消息 const MixElement = sendElements.filter( @@ -213,7 +268,7 @@ export class SendMsg extends BaseAction { continue; } const nodeMsg = (await this.core.apis.MsgApi.getMsgsByMsgId(nodeMsgPeer.Peer, [msgId])).msgList[0]; - srcPeer = srcPeer ?? { chatType: nodeMsg.chatType, peerUid: nodeMsg.peerUid }; + srcPeer = srcPeer ?? {chatType: nodeMsg.chatType, peerUid: nodeMsg.peerUid}; if (srcPeer.peerUid !== nodeMsg.peerUid) { needSendSelf = true; } @@ -236,10 +291,14 @@ export class SendMsg extends BaseAction { if (retMsgIds.length === 0) throw Error('转发消息失败,生成节点为空'); try { logger.logDebug('开发转发', srcPeer, destPeer, retMsgIds); - return await this.core.apis.MsgApi.multiForwardMsg(srcPeer!, destPeer, retMsgIds); + return { + message: await this.core.apis.MsgApi.multiForwardMsg(srcPeer!, destPeer, retMsgIds) + }; } catch (e) { logger.logError.bind(this.core.context.logger)('forward failed', e); - return null; + return { + message: null + }; } } diff --git a/src/onebot/action/packet/GetPacketStatus.ts b/src/onebot/action/packet/GetPacketStatus.ts new file mode 100644 index 00000000..d5f48c89 --- /dev/null +++ b/src/onebot/action/packet/GetPacketStatus.ts @@ -0,0 +1,25 @@ +import BaseAction from '../BaseAction'; +import {ActionName, BaseCheckResult} from '../types'; + + +export abstract class GetPacketStatusDepends extends BaseAction { + actionName = ActionName.GetPacketStatus; + + protected async check(): Promise{ + if (!this.core.apis.PacketApi.available) { + return { + valid: false, + message: "PacketClient is not available!", + } + } + return { + valid: true, + } + } +} + +export class GetPacketStatus extends GetPacketStatusDepends { + async _handle(payload: any) { + return null + } +} diff --git a/src/onebot/action/types.ts b/src/onebot/action/types.ts index 7ad25cbc..aff90d0f 100644 --- a/src/onebot/action/types.ts +++ b/src/onebot/action/types.ts @@ -69,7 +69,7 @@ export enum ActionName { GetRecord = 'get_record', CleanCache = 'clean_cache', GetCookies = 'get_cookies', - // 以下为go-cqhttp api + // 以下为go-cqhttp api GoCQHTTP_HandleQuickAction = '.handle_quick_operation', GetGroupHonorInfo = 'get_group_honor_info', GoCQHTTP_GetEssenceMsg = 'get_essence_msg_list', @@ -85,6 +85,7 @@ export enum ActionName { MarkGroupMsgAsRead = 'mark_group_msg_as_read', GoCQHTTP_UploadGroupFile = 'upload_group_file', GOCQHTTP_DeleteGroupFile = 'delete_group_file', + GOCQHTTP_GetGroupFileUrl = 'get_group_file_url', GoCQHTTP_CreateGroupFileFolder = 'create_group_file_folder', GoCQHTTP_DeleteGroupFileFolder = 'delete_group_file_folder', GoCQHTTP_GetGroupFileSystemInfo = 'get_group_file_system_info', @@ -120,4 +121,10 @@ export enum ActionName { GetGroupInfoEx = "get_group_info_ex", GetGroupSystemMsg = 'get_group_system_msg', FetchUserProfileLike = "fetch_user_profile_like", + GetPacketStatus = 'nc_get_packet_status', + GetUserStatus = "nc_get_user_status", + GetRkey = "nc_get_rkey", + SetSpecialTittle = "set_group_special_title", + // UploadForwardMsg = "upload_forward_msg", + GetGroupShutList = "get_goup_shut_list", } diff --git a/src/onebot/action/user/SetFriendAddRequest.ts b/src/onebot/action/user/SetFriendAddRequest.ts index 02eb0661..a06f0fb2 100644 --- a/src/onebot/action/user/SetFriendAddRequest.ts +++ b/src/onebot/action/user/SetFriendAddRequest.ts @@ -21,6 +21,14 @@ export default class SetFriendAddRequest extends BaseAction { async _handle(payload: Payload): Promise { const approve = payload.approve?.toString() !== 'false'; await this.core.apis.FriendApi.handleFriendRequest(payload.flag, approve); + if (payload.remark) { + const data = payload.flag.split('|'); + if (data.length < 2) { + throw new Error('Invalid flag'); + } + const friendUid = data[0]; + await this.core.apis.FriendApi.setBuddyRemark(friendUid, payload.remark); + } return null; } } diff --git a/src/onebot/api/group.ts b/src/onebot/api/group.ts index 30a9222b..ca5ed920 100644 --- a/src/onebot/api/group.ts +++ b/src/onebot/api/group.ts @@ -79,7 +79,7 @@ export class OneBotGroupApi { id: FileNapCatOneBotUUID.encode({ chatType: ChatType.KCHATTYPEGROUP, peerUid: msg.peerUid, - }, msg.msgId, element.elementId, "." + element.fileElement.fileName), + }, msg.msgId, element.elementId, element.fileElement.fileUuid, "." + element.fileElement.fileName), url: pathToFileURL(element.fileElement.filePath).href, name: element.fileElement.fileName, size: parseInt(element.fileElement.fileSize), diff --git a/src/onebot/api/msg.ts b/src/onebot/api/msg.ts index 61279f9c..e93a5a48 100644 --- a/src/onebot/api/msg.ts +++ b/src/onebot/api/msg.ts @@ -26,15 +26,15 @@ import { OB11MessageFileBase, OB11MessageForward, } from '@/onebot'; -import { OB11Entities } from '@/onebot/entities'; -import { EventType } from '@/onebot/event/OB11BaseEvent'; -import { encodeCQCode } from '@/onebot/cqcode'; -import { uri2local } from '@/common/file'; -import { RequestUtil } from '@/common/request'; +import {OB11Entities} from '@/onebot/entities'; +import {EventType} from '@/onebot/event/OB11BaseEvent'; +import {encodeCQCode} from '@/onebot/cqcode'; +import {uri2local} from '@/common/file'; +import {RequestUtil} from '@/common/request'; import fs from 'node:fs'; import fsPromise from 'node:fs/promises'; -import { OB11FriendAddNoticeEvent } from '@/onebot/event/notice/OB11FriendAddNoticeEvent'; -import { decodeSysMessage } from '@/core/proto/ProfileLike'; +import {OB11FriendAddNoticeEvent} from '@/onebot/event/notice/OB11FriendAddNoticeEvent'; +import {decodeSysMessage} from '@/core/packet/proto/old/ProfileLike'; type RawToOb11Converters = { [Key in keyof MessageElement as Key extends `${string}Element` ? Key : never]: ( @@ -108,10 +108,11 @@ export class OneBotMsgApi { peerUid: msg.peerUid, guildId: '', }; - const encodedFileId = FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, "." + element.fileName); + const encodedFileId = FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, element.fileUuid, "." + element.fileName); return { type: OB11MessageDataType.image, data: { + summary: element.summary, file: encodedFileId, sub_type: element.picSubType, file_id: encodedFileId, @@ -139,7 +140,7 @@ export class OneBotMsgApi { file: element.fileName, path: element.filePath, url: pathToFileURL(element.filePath).href, - file_id: FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, "." + element.fileName), + file_id: FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, element.fileUuid,"." + element.fileName), file_size: element.fileSize, file_unique: element.fileName, }, @@ -166,7 +167,7 @@ export class OneBotMsgApi { return { type: OB11MessageDataType.face, data: { - id: element.faceIndex.toString(), + id: element.faceIndex.toString() }, }; } @@ -184,8 +185,9 @@ export class OneBotMsgApi { return { type: OB11MessageDataType.image, data: { + summary: _.faceName, // 商城表情名称 file: 'marketface', - file_id: FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, "." + _.key + ".jpg"), + file_id: FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, "", "." + _.key + ".jpg"), path: url, url: url, file_unique: _.key @@ -273,7 +275,7 @@ export class OneBotMsgApi { if (!videoDownUrl) { videoDownUrl = element.filePath; } - const fileCode = FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, "." + element.fileName); + const fileCode = FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, "", "." + element.fileName); return { type: OB11MessageDataType.video, data: { @@ -293,7 +295,7 @@ export class OneBotMsgApi { peerUid: msg.peerUid, guildId: '', }; - const fileCode = FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, "." + element.fileName); + const fileCode = FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, "", "." + element.fileName); return { type: OB11MessageDataType.voice, data: { @@ -495,8 +497,7 @@ export class OneBotMsgApi { const uri2LocalRes = await uri2local(this.core.NapCatTempPath, thumb); if (uri2LocalRes.success) thumb = uri2LocalRes.path; } - const videoEle = await this.core.apis.FileApi.createValidSendVideoElement(context, path, fileName, thumb); - return videoEle; + return await this.core.apis.FileApi.createValidSendVideoElement(context, path, fileName, thumb); }, [OB11MessageDataType.voice]: async (sendMsg, context) => @@ -694,8 +695,9 @@ export class OneBotMsgApi { resMsg.sub_type = 'group'; const ret = await this.core.apis.MsgApi.getTempChatInfo(ChatType.KCHATTYPETEMPC2CFROMGROUP, msg.senderUid); if (ret.result === 0) { + const member = await this.core.apis.GroupApi.getGroupMember(msg.peerUin, msg.senderUin); resMsg.group_id = parseInt(ret.tmpChatInfo!.groupCode); - resMsg.sender.nickname = ret.tmpChatInfo!.fromNick; + resMsg.sender.nickname = member?.nick ?? member?.cardName ?? '临时会话'; resMsg.temp_source = resMsg.group_id; } else { resMsg.group_id = 284840486; //兜底数据 diff --git a/src/onebot/api/user.ts b/src/onebot/api/user.ts index 716a5803..7c3c09d1 100644 --- a/src/onebot/api/user.ts +++ b/src/onebot/api/user.ts @@ -1,5 +1,5 @@ import { NapCatCore } from '@/core'; -import { decodeProfileLikeTip } from '@/core/proto/ProfileLike'; +import { decodeProfileLikeTip } from '@/core/packet/proto/old/ProfileLike'; import { NapCatOneBot11Adapter } from '@/onebot'; import { OB11ProfileLikeEvent } from '../event/notice/OB11ProfileLikeEvent'; diff --git a/src/onebot/cqcode.ts b/src/onebot/cqcode.ts index 9c830166..e2f0152b 100644 --- a/src/onebot/cqcode.ts +++ b/src/onebot/cqcode.ts @@ -15,10 +15,12 @@ function from(source: string) { if (!capture) return null; const [, type, attrs] = capture; const data: Record = {}; - attrs && attrs.slice(1).split(',').forEach((str) => { - const index = str.indexOf('='); - data[str.slice(0, index)] = unescape(str.slice(index + 1)); - }); + if (attrs) { + attrs.slice(1).split(',').forEach((str) => { + const index = str.indexOf('='); + data[str.slice(0, index)] = unescape(str.slice(index + 1)); + }); + } return { type, data, capture }; } diff --git a/src/onebot/entities.ts b/src/onebot/entities.ts index 93a7c414..1f7c911b 100644 --- a/src/onebot/entities.ts +++ b/src/onebot/entities.ts @@ -66,10 +66,10 @@ export class OB11Entities { sex: OB11Entities.sex(member.sex!), age: member.age ?? 0, area: '', - level: '0', + level: member.memberRealLevel ?? '0', qq_level: member.qqLevel && calcQQLevel(member.qqLevel) || 0, - join_time: 0, // 暂时没法获取 - last_sent_time: 0, // 暂时没法获取 + join_time: +member.joinTime, + last_sent_time: +member.lastSpeakTime, title_expire_time: 0, unfriendly: false, card_changeable: true, @@ -77,6 +77,7 @@ export class OB11Entities { shut_up_timestamp: member.shutUpTime, role: OB11Entities.groupMemberRole(member.role), title: member.memberSpecialTitle || '', + }; } diff --git a/src/onebot/index.ts b/src/onebot/index.ts index 0882589b..43af0253 100644 --- a/src/onebot/index.ts +++ b/src/onebot/index.ts @@ -45,7 +45,7 @@ import { OB11GroupRecallNoticeEvent } from '@/onebot/event/notice/OB11GroupRecal import { LRUCache } from '@/common/lru-cache'; import { NodeIKernelRecentContactListener } from '@/core/listeners/NodeIKernelRecentContactListener'; import { Native } from '@/native'; -import { decodeMessage, decodeRecallGroup, Message, RecallGroup } from '@/core/proto/Message'; +import { decodeMessage, decodeRecallGroup } from '@/core/packet/proto/old/Message'; //OneBot实现类 export class NapCatOneBot11Adapter { @@ -84,19 +84,19 @@ export class NapCatOneBot11Adapter { if (!this.nativeCore.inited) throw new Error('Native Not Init'); this.nativeCore.registerRecallCallback(async (hex: string) => { try { - let data = decodeMessage(Buffer.from(hex, 'hex')) as any; + const data = decodeMessage(Buffer.from(hex, 'hex')); //data.MsgHead.BodyInner.MsgType SubType - let bodyInner = data.msgHead?.bodyInner; + const bodyInner = data.msgHead?.bodyInner; //context.logger.log("[appNative] Parse MsgType:" + bodyInner.msgType + " / SubType:" + bodyInner.subType); if (bodyInner && bodyInner.msgType == 732 && bodyInner.subType == 17) { - let RecallData = Buffer.from(data.msgHead.noifyData.innerData); + const RecallData = Buffer.from(data.msgHead.noifyData.innerData); //跳过 4字节 群号 + 不知道的1字节 +2字节 长度 - let uid = RecallData.readUint32BE(); + const uid = RecallData.readUint32BE(); const buffer = Buffer.from(RecallData.toString('hex').slice(14), 'hex'); - let seq: number = decodeRecallGroup(buffer).recallDetails.subDetail.msgSeq; - let peer: Peer = { chatType: ChatType.KCHATTYPEGROUP, peerUid: uid.toString() }; + const seq: number = decodeRecallGroup(buffer).recallDetails.subDetail.msgSeq; + const peer: Peer = { chatType: ChatType.KCHATTYPEGROUP, peerUid: uid.toString() }; context.logger.log("[Native] 群消息撤回 Peer: " + uid.toString() + " / MsgSeq:" + seq); - let msgs = await core.apis.MsgApi.queryMsgsWithFilterExWithSeq(peer, seq.toString()); + const msgs = await core.apis.MsgApi.queryMsgsWithFilterExWithSeq(peer, seq.toString()); this.recallMsgCache.put(msgs.msgList[0].msgId, msgs.msgList[0]); } } catch (error: any) { @@ -540,9 +540,35 @@ export class NapCatOneBot11Adapter { if (isSelfMsg) { ob11Msg.target_id = parseInt(message.peerUin); } - // if(ob11Msg.raw_message.startsWith('!poke')){ - // console.log('poke',message.peerUin, message.senderUin); - // this.core.apis.GroupApi.sendPacketPoke(message.peerUin, message.senderUin); + // if (ob11Msg.raw_message.startsWith('!set')) { + // this.core.apis.UserApi.getUidByUinV2(ob11Msg.user_id.toString()).then(uid => { + // if(uid){ + // this.core.apis.PacketApi.sendSetSpecialTittlePacket(message.peerUin, uid, '测试'); + // console.log('set', message.peerUin, uid); + // } + + // }); + + // } + // if (ob11Msg.raw_message.startsWith('!status')) { + // console.log('status', message.peerUin, message.senderUin); + // let delMsg: string[] = []; + // let peer = { + // peerUid: message.peerUin, + // chatType: 2, + // }; + // this.core.apis.PacketApi.sendStatusPacket(+message.senderUin).then(async e => { + // if (e) { + // const { sendElements } = await this.apis.MsgApi.createSendElements([{ + // type: OB11MessageDataType.text, + // data: { + // text: 'status ' + JSON.stringify(e, null, 2), + // } + // }], peer) + + // this.apis.MsgApi.sendMsgWithOb11UniqueId(peer, sendElements, delMsg) + // } + // }) // } this.networkManager.emitEvent(ob11Msg); }).catch(e => this.context.logger.logError.bind(this.context.logger)('constructMessage error: ', e)); @@ -564,12 +590,13 @@ export class NapCatOneBot11Adapter { private async emitRecallMsg(msgList: RawMessage[], cache: LRUCache) { for (const message of msgList) { // log("message update", message.sendStatus, message.msgId, message.msgSeq) + const peer: Peer = { chatType: message.chatType, peerUid: message.peerUid, guildId: '' }; if (message.recallTime != '0' && !cache.get(message.msgId)) { //todo: 这个判断方法不太好,应该使用灰色消息元素来判断? cache.put(message.msgId, true); // 撤回消息上报 - const oriMessageId = MessageUnique.getShortIdByMsgId(message.msgId); + let oriMessageId = MessageUnique.getShortIdByMsgId(message.msgId); if (!oriMessageId) { - continue; + oriMessageId = MessageUnique.createUniqueMsgId(peer, message.msgId); } if (message.chatType == ChatType.KCHATTYPEC2C) { const friendRecallEvent = new OB11FriendRecallNoticeEvent( diff --git a/src/onebot/network/passive-http.ts b/src/onebot/network/passive-http.ts index 0a7b8367..44af0843 100644 --- a/src/onebot/network/passive-http.ts +++ b/src/onebot/network/passive-http.ts @@ -64,6 +64,7 @@ export class OB11PassiveHttpAdapter implements IOB11NetworkAdapter { }); this.app.use((req, res, next) => this.authorize(this.token, req, res, next)); + // @ts-ignore this.app.use((req, res) => this.handleRequest(req, res)); this.server.listen(this.port, () => { diff --git a/src/onebot/types/message.ts b/src/onebot/types/message.ts index 0ef01c9b..93ab4180 100644 --- a/src/onebot/types/message.ts +++ b/src/onebot/types/message.ts @@ -154,6 +154,13 @@ export interface OB11MessageNode { }; } +export type OB11MessageNodePlain = OB11MessageNode & { + data: { + content?: Array; + message: Array; + }; +}; + export interface OB11MessageIdMusic { type: OB11MessageDataType.music; data: IdMusicSignPostData; diff --git a/src/webui/ui/NapCat.ts b/src/webui/ui/NapCat.ts index cb41849c..31095bee 100644 --- a/src/webui/ui/NapCat.ts +++ b/src/webui/ui/NapCat.ts @@ -30,7 +30,7 @@ async function onSettingWindowCreated(view: Element) { SettingItem( 'Napcat', undefined, - SettingButton('V2.6.27', 'napcat-update-button', 'secondary'), + SettingButton('V3.0.0', 'napcat-update-button', 'secondary'), ), ]), SettingList([ diff --git a/static/assets/renderer.js b/static/assets/renderer.js index ee924be0..688be742 100644 --- a/static/assets/renderer.js +++ b/static/assets/renderer.js @@ -164,7 +164,7 @@ async function onSettingWindowCreated(view) { SettingItem( 'Napcat', void 0, - SettingButton("V2.6.27", "napcat-update-button", "secondary") + SettingButton("V3.0.0", "napcat-update-button", "secondary") ) ]), SettingList([