Merge pull request #434 from NapNeko/dev-packet

Dev packet
This commit is contained in:
手瓜一十雪
2024-10-12 15:38:16 +08:00
committed by GitHub
15 changed files with 292 additions and 11 deletions

View File

@@ -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,
};

View File

@@ -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<string, Map<string, GroupMember>> = new Map<string, Map<string, GroupMember>>();
groups: Group[] = [];
essenceLRU = new LimitedHashTable<number, string>(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({

77
src/core/apis/packet.ts Normal file
View File

@@ -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<undefined | {
type: string,//仅recv含有data
trace_id: string,
data: {
trace_id: string,
seq: number,
hex_data: string,
cmd: string
}
}>((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));
});
}
}

View File

@@ -2,5 +2,6 @@
"fileLog": true,
"consoleLog": true,
"fileLogLevel": "debug",
"consoleLogLevel": "info"
}
"consoleLogLevel": "info",
"packetServer": ""
}

10
src/core/external/offset.json vendored Normal file
View File

@@ -0,0 +1,10 @@
{
"9.9.15-28418":{
"recv": "37A9004",
"send": "37A4BD0"
},
"9.9.15-28498":{
"recv": "37A9004",
"send": "37A4BD0"
}
}

116
src/core/helper/packet.ts Normal file
View File

@@ -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<string, { type: string, callback: any }>(500);
constructor(private url: string, public logger: LogWrapper) { }
connect(): Promise<void> {
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<void> {
this.cb.put(trace_id, { type: type, callback: callback });
}
async init(pid: number, recv: string, send: string): Promise<void> {
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<any> {
return new Promise<any>((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<void> {
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}`);
}
}
}

View File

@@ -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,

View File

@@ -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
});
}

View File

@@ -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)

View File

@@ -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<typeof SchemaData>;
export class GroupPoke extends BaseAction<Payload, any> {
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);
}
}

View File

@@ -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<string, BaseAction<any, any>>;
@@ -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) {

View File

@@ -16,6 +16,7 @@ export interface InvalidCheckResult {
export enum ActionName {
// 以下为扩展napcat扩展
Unknown = 'unknown',
GroupPoke = 'group_poke',
SharePeer = 'ArkSharePeer',
ShareGroupEx = 'ArkShareGroup',
RebootNormal = 'reboot_normal',//无快速登录重新启动

View File

@@ -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;

View File

@@ -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
},

View File

@@ -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));