mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2025-07-19 12:03:37 +00:00
@@ -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,
|
||||
};
|
||||
|
@@ -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
77
src/core/apis/packet.ts
Normal 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));
|
||||
});
|
||||
}
|
||||
}
|
3
src/core/external/napcat.json
vendored
3
src/core/external/napcat.json
vendored
@@ -2,5 +2,6 @@
|
||||
"fileLog": true,
|
||||
"consoleLog": true,
|
||||
"fileLogLevel": "debug",
|
||||
"consoleLogLevel": "info"
|
||||
"consoleLogLevel": "info",
|
||||
"packetServer": ""
|
||||
}
|
10
src/core/external/offset.json
vendored
Normal file
10
src/core/external/offset.json
vendored
Normal 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
116
src/core/helper/packet.ts
Normal 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}`);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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,
|
||||
|
21
src/core/proto/Oidb.fe1_2.ts
Normal file
21
src/core/proto/Oidb.fe1_2.ts
Normal 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
|
||||
});
|
||||
}
|
@@ -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)
|
||||
|
26
src/onebot/action/group/GroupPoke.ts
Normal file
26
src/onebot/action/group/GroupPoke.ts
Normal 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);
|
||||
}
|
||||
}
|
@@ -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) {
|
||||
|
@@ -16,6 +16,7 @@ export interface InvalidCheckResult {
|
||||
export enum ActionName {
|
||||
// 以下为扩展napcat扩展
|
||||
Unknown = 'unknown',
|
||||
GroupPoke = 'group_poke',
|
||||
SharePeer = 'ArkSharePeer',
|
||||
ShareGroupEx = 'ArkShareGroup',
|
||||
RebootNormal = 'reboot_normal',//无快速登录重新启动
|
||||
|
@@ -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;
|
||||
|
@@ -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
|
||||
},
|
||||
|
@@ -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));
|
||||
|
||||
|
Reference in New Issue
Block a user