diff --git a/README.md b/README.md index c13fe682..f3116d5b 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ NapCatQQ (aka 猫猫框架) 是现代化的基于 NTQQ 的 Bot 协议端实现 [Telegram Link](https://t.me/+nLZEnpne-pQ1OWFl) ## 猫猫朋友 -感谢 [LLOneBot](https://github.com/LLOneBot/LLOneBot) 提供部分参考 +感谢 [LLOneBot](https://github.com/LLOneBot/LLOneBot) 感谢 [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) 对本项目的大力支持 diff --git a/manifest.json b/manifest.json index f6dc5a30..360bb5e7 100644 --- a/manifest.json +++ b/manifest.json @@ -4,7 +4,7 @@ "name": "NapCatQQ", "slug": "NapCat.Framework", "description": "高性能的 OneBot 11 协议实现", - "version": "2.6.24", + "version": "2.6.27", "icon": "./logo.png", "authors": [ { diff --git a/package.json b/package.json index 0103933b..95ca3044 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "napcat", "private": true, "type": "module", - "version": "2.6.24", + "version": "2.6.27", "scripts": { "build:framework": "vite build --mode framework", "build:shell": "vite build --mode shell", diff --git a/src/common/lru-cache.ts b/src/common/lru-cache.ts index 1cb54212..0e537b6a 100644 --- a/src/common/lru-cache.ts +++ b/src/common/lru-cache.ts @@ -24,7 +24,9 @@ export class LRUCache { } else if (this.cache.size >= this.capacity) { // If the cache is full, remove the least recently used key (the first one in the map) const firstKey = this.cache.keys().next().value; - this.cache.delete(firstKey); + if (firstKey !== undefined) { + this.cache.delete(firstKey); + } } this.cache.set(key, value); } diff --git a/src/common/request.ts b/src/common/request.ts index 25b759fe..f3e1cebe 100644 --- a/src/common/request.ts +++ b/src/common/request.ts @@ -61,7 +61,7 @@ export class RequestUtil { const options = { hostname: option.hostname, port: option.port, - path: option.href, + path: option.pathname + option.search, method: method, headers: headers, }; diff --git a/src/common/version.ts b/src/common/version.ts index 17b03927..63a0246c 100644 --- a/src/common/version.ts +++ b/src/common/version.ts @@ -1 +1 @@ -export const napCatVersion = '2.6.24'; +export const napCatVersion = '2.6.27'; diff --git a/src/core/apis/group.ts b/src/core/apis/group.ts index f4209788..307ab3ab 100644 --- a/src/core/apis/group.ts +++ b/src/core/apis/group.ts @@ -9,10 +9,22 @@ import { MemberExtSourceType, NapCatCore, } from '@/core'; -import { isNumeric, solveAsyncProblem } from '@/common/helper'; +import { isNumeric, sleep, 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; @@ -20,6 +32,7 @@ export class NTQQGroupApi { groupMemberCache: Map> = new Map>(); groups: Group[] = []; essenceLRU = new LimitedHashTable(1000); + session: any; constructor(context: InstanceContext, core: NapCatCore) { this.context = context; @@ -33,6 +46,11 @@ 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); } async getCoreAndBaseInfo(uids: string[]) { return await this.core.eventWrapper.callNoListenerEvent( @@ -41,6 +59,17 @@ 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({ diff --git a/src/core/apis/packet.ts b/src/core/apis/packet.ts new file mode 100644 index 00000000..6666b871 --- /dev/null +++ b/src/core/apis/packet.ts @@ -0,0 +1,68 @@ +import { InstanceContext, NapCatCore } from '..'; +import { RequestUtil } from '@/common/request'; +import offset from '@/core/external/offset.json'; +import * as crypto from 'crypto'; +import { PacketClient } from '../helper/packet'; + +interface OffsetType { + [key: string]: { + recv: string; + send: string; + }; +} + +const typedOffset: OffsetType = offset; +export class NTQQPacketApi { + context: InstanceContext; + core: NapCatCore; + serverUrl: string | undefined; + qqversion: string | undefined; + isInit: boolean = false; + PacketClient: PacketClient | undefined; + constructor(context: InstanceContext, core: NapCatCore) { + this.context = context; + this.core = core; + let 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()) + .then() + .catch(this.core.context.logger.logError.bind(this.core.context.logger)); + } + } + 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; + } + 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: 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; + } + 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)); + }); + } +} \ No newline at end of file diff --git a/src/core/external/appid.json b/src/core/external/appid.json index 76785604..84928941 100644 --- a/src/core/external/appid.json +++ b/src/core/external/appid.json @@ -38,5 +38,9 @@ "6.9.56-28418": { "appid": 537249367, "qua": "V1_MAC_NQ_6.9.56_28418_GW_B" + }, + "9.9.15-28498":{ + "appid": 537249321, + "qua": "V1_WIN_NQ_9.9.15_28498_GW_B" } } diff --git a/src/core/external/napcat.json b/src/core/external/napcat.json index 1c822077..92432677 100644 --- a/src/core/external/napcat.json +++ b/src/core/external/napcat.json @@ -2,5 +2,6 @@ "fileLog": true, "consoleLog": true, "fileLogLevel": "debug", - "consoleLogLevel": "info" -} + "consoleLogLevel": "info", + "packetServer": "" +} \ No newline at end of file diff --git a/src/core/external/offset.json b/src/core/external/offset.json new file mode 100644 index 00000000..2956b666 --- /dev/null +++ b/src/core/external/offset.json @@ -0,0 +1,14 @@ +{ + "3.2.12-28418": { + "recv": "A0723E0", + "send": "A06EAE0" + }, + "9.9.15-28418": { + "recv": "37A9004", + "send": "37A4BD0" + }, + "9.9.15-28498": { + "recv": "37A9004", + "send": "37A4BD0" + } +} \ No newline at end of file diff --git a/src/core/helper/packet.ts b/src/core/helper/packet.ts new file mode 100644 index 00000000..b287db42 --- /dev/null +++ b/src/core/helper/packet.ts @@ -0,0 +1,123 @@ +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/index.ts b/src/core/index.ts index 60ff732b..53c50248 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -31,6 +31,7 @@ import { NodeIKernelGroupListener, NodeIKernelMsgListener, NodeIKernelProfileLis import { proxiedListenerOf } from '@/common/proxy-handler'; import { NapCatEventChannel } from '@/core/events'; +import { NTQQPacketApi } from './apis/packet'; export * from './wrapper'; export * from './entities'; export * from './services'; @@ -84,17 +85,18 @@ export class NapCatCore { this.context = context; this.util = this.context.wrapper.NodeQQNTWrapperUtil; this.eventWrapper = new NTEventWrapper(context.session); + this.configLoader = new NapCatConfigLoader(this, this.context.pathWrapper.configPath); this.apis = { FileApi: new NTQQFileApi(this.context, this), SystemApi: new NTQQSystemApi(this.context, this), CollectionApi: new NTQQCollectionApi(this.context, this), + PacketApi: new NTQQPacketApi(this.context, this), WebApi: new NTQQWebApi(this.context, this), FriendApi: new NTQQFriendApi(this.context, this), MsgApi: new NTQQMsgApi(this.context, this), UserApi: new NTQQUserApi(this.context, this), GroupApi: new NTQQGroupApi(this.context, this), }; - this.configLoader = new NapCatConfigLoader(this, this.context.pathWrapper.configPath); this.NapCatDataPath = path.join(this.dataPath, 'NapCat'); fs.mkdirSync(this.NapCatDataPath, { recursive: true }); this.NapCatTempPath = path.join(this.NapCatDataPath, 'temp'); @@ -328,6 +330,7 @@ export interface InstanceContext { export interface StableNTApiWrapper { FileApi: NTQQFileApi, SystemApi: NTQQSystemApi, + PacketApi: NTQQPacketApi, CollectionApi: NTQQCollectionApi, WebApi: NTQQWebApi, FriendApi: NTQQFriendApi, diff --git a/src/core/proto/Oidb.fe1_2.ts b/src/core/proto/Oidb.fe1_2.ts new file mode 100644 index 00000000..2af198c6 --- /dev/null +++ b/src/core/proto/Oidb.fe1_2.ts @@ -0,0 +1,21 @@ +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 new file mode 100644 index 00000000..3829d4ef --- /dev/null +++ b/src/core/proto/Poke.ts @@ -0,0 +1,31 @@ +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/native/index.ts b/src/native/index.ts index ef504380..97fed730 100644 --- a/src/native/index.ts +++ b/src/native/index.ts @@ -4,7 +4,7 @@ import { dlopen } from "process"; import fs from "fs"; export class Native { platform: string; - supportedPlatforms = ['win32']; + supportedPlatforms = ['']; MoeHooExport: any = { exports: {} }; recallHookEnabled: boolean = false; inited = true; diff --git a/src/onebot/action/group/GroupPoke.ts b/src/onebot/action/group/GroupPoke.ts new file mode 100644 index 00000000..6f960776 --- /dev/null +++ b/src/onebot/action/group/GroupPoke.ts @@ -0,0 +1,26 @@ +import BaseAction from '../BaseAction'; +import { ActionName } from '../types'; +import { FromSchema, JSONSchema } from 'json-schema-to-ts'; +// no_cache get时传字符串 +const SchemaData = { + type: 'object', + properties: { + group_id: { type: ['number', 'string'] }, + user_id: { type: ['number', 'string'] }, + }, + required: ['group_id', 'user_id'], +} as const satisfies JSONSchema; + +type Payload = FromSchema; + +export class GroupPoke extends BaseAction { + 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); + } +} \ No newline at end of file diff --git a/src/onebot/action/index.ts b/src/onebot/action/index.ts index edbeffb1..6bb72665 100644 --- a/src/onebot/action/index.ts +++ b/src/onebot/action/index.ts @@ -84,6 +84,7 @@ import { GetGroupFileSystemInfo } from '@/onebot/action/go-cqhttp/GetGroupFileSy 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'; export type ActionMap = Map>; @@ -180,6 +181,7 @@ export function createActionMap(obContext: NapCatOneBot11Adapter, core: NapCatCo new GetGroupFilesByFolder(obContext, core), new GetGroupSystemMsg(obContext, core), new FetchUserProfileLike(obContext, core), + new GroupPoke(obContext, core), ]; const actionMap = new Map(); for (const action of actionHandlers) { diff --git a/src/onebot/action/types.ts b/src/onebot/action/types.ts index 77c153f8..7ad25cbc 100644 --- a/src/onebot/action/types.ts +++ b/src/onebot/action/types.ts @@ -16,6 +16,7 @@ export interface InvalidCheckResult { export enum ActionName { // 以下为扩展napcat扩展 Unknown = 'unknown', + GroupPoke = 'group_poke', SharePeer = 'ArkSharePeer', ShareGroupEx = 'ArkShareGroup', RebootNormal = 'reboot_normal',//无快速登录重新启动 diff --git a/src/onebot/api/group.ts b/src/onebot/api/group.ts index 013c944c..30a9222b 100644 --- a/src/onebot/api/group.ts +++ b/src/onebot/api/group.ts @@ -21,6 +21,7 @@ import { OB11GroupTitleEvent } from '@/onebot/event/notice/OB11GroupTitleEvent'; import { FileNapCatOneBotUUID } from '@/common/helper'; import { pathToFileURL } from 'node:url'; + export class OneBotGroupApi { obContext: NapCatOneBot11Adapter; core: NapCatCore; diff --git a/src/onebot/index.ts b/src/onebot/index.ts index 8f7912eb..0882589b 100644 --- a/src/onebot/index.ts +++ b/src/onebot/index.ts @@ -540,6 +540,10 @@ 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); + // } this.networkManager.emitEvent(ob11Msg); }).catch(e => this.context.logger.logError.bind(this.context.logger)('constructMessage error: ', e)); diff --git a/src/webui/ui/NapCat.ts b/src/webui/ui/NapCat.ts index 0b0e310e..cb41849c 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.24', 'napcat-update-button', 'secondary'), + SettingButton('V2.6.27', 'napcat-update-button', 'secondary'), ), ]), SettingList([ diff --git a/static/assets/renderer.js b/static/assets/renderer.js index 4c7b2814..ee924be0 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.24", "napcat-update-button", "secondary") + SettingButton("V2.6.27", "napcat-update-button", "secondary") ) ]), SettingList([