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/core/apis/group.ts b/src/core/apis/group.ts index f4209788..84f0f2c1 100644 --- a/src/core/apis/group.ts +++ b/src/core/apis/group.ts @@ -9,9 +9,12 @@ 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'; export class NTQQGroupApi { context: InstanceContext; @@ -20,6 +23,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 +37,14 @@ 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 () => { + // let data = Buffer.from('089601', 'hex').toString('utf-8');//optional int32 a = 1; + // console.log('data', Buffer.from(data).toString('hex')); + // let ret = await this.core.context.session.getMsgService().sendSsoCmdReqByContend("OidbSvcTrpcTcp.0xfe1_2", data); + // console.log('sendSsoCmdReqByContend', ret); + // }, 20000); } async getCoreAndBaseInfo(uids: string[]) { return await this.core.eventWrapper.callNoListenerEvent( @@ -41,6 +53,12 @@ export class NTQQGroupApi { uids, ); } + 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..62e78f10 --- /dev/null +++ b/src/core/apis/packet.ts @@ -0,0 +1,77 @@ +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/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..b3193345 --- /dev/null +++ b/src/core/external/offset.json @@ -0,0 +1,10 @@ +{ + "9.9.15-28418":{ + "recv": "37A9004", + "send": "37A4BD0" + }, + "9.9.15-28498":{ + "recv": "37A9004", + "send": "37A4BD0" + } +} diff --git a/src/core/helper/packet.ts b/src/core/helper/packet.ts new file mode 100644 index 00000000..05b7b710 --- /dev/null +++ b/src/core/helper/packet.ts @@ -0,0 +1,116 @@ +import { LogWrapper } from "@/common/log"; +import { LRUCache } from "@/common/lru-cache"; +import WebSocket from "ws"; + +export class PacketClient { + private websocket: WebSocket | undefined; + public isConnected: boolean = false; + private reconnectAttempts: number = 0; + private maxReconnectAttempts: number = 5; + 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 { + 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}`); + } + } + async registerCallback(trace_id: string, type: string, callback: any): Promise { + this.cb.put(trace_id, { type: type, callback: 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 = json.trace_id; + let event = this.cb.get(trace_id); + if (event?.type == 'all' || event?.type == json.type) { + await event?.callback(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 5afa28a7..338753f3 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -29,6 +29,7 @@ import { NapCatConfigLoader } from '@/core/helper/config'; import os from 'node:os'; import { NodeIKernelGroupListener, NodeIKernelMsgListener, NodeIKernelProfileListener } from '@/core/listeners'; import { proxiedListenerOf } from '@/common/proxy-handler'; +import { NTQQPacketApi } from './apis/packet'; export * from './wrapper'; export * from './entities'; export * from './services'; @@ -80,17 +81,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'); @@ -322,6 +324,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 index ebd630f4..3829d4ef 100644 --- a/src/core/proto/Poke.ts +++ b/src/core/proto/Poke.ts @@ -3,7 +3,8 @@ 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: 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", [ @@ -13,11 +14,11 @@ export const OidbSvcTrpcTcp0XED3_1 = new MessageType("oidb_svc_trpctcp_0xed3_1", { no: 6, name: "ext", kind: "scalar", T: ScalarType.UINT32 } ]); -export function encodeGroupPoke(groupUin: string, PeerUin: string) { +export function encodeGroupPoke(groupUin: number, PeerUin: number) { let Body = OidbSvcTrpcTcp0XED3_1.toBinary ({ - uin: parseInt(PeerUin), - groupuin: parseInt(groupUin), + uin: PeerUin, + groupuin: groupUin, ext: 0 }); //console.log(Body) 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/config/onebot11.json b/src/onebot/config/onebot11.json index 8ee5a368..e6823634 100644 --- a/src/onebot/config/onebot11.json +++ b/src/onebot/config/onebot11.json @@ -1,6 +1,6 @@ { "http": { - "enable": false, + "enable": true, "host": "", "port": 3000, "secret": "", @@ -9,7 +9,7 @@ "postUrls": [] }, "ws": { - "enable": false, + "enable": true, "host": "", "port": 3001 }, 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));