refactor: hook some api

This commit is contained in:
linyuchen 2024-02-08 01:45:56 +08:00
parent 3c4db1d9d9
commit 2c55d84b9f
9 changed files with 351 additions and 38 deletions

View File

@ -12,7 +12,7 @@ export enum ChatType {
temp = 100 temp = 100
} }
export interface GroupMemberInfo { export interface GroupMember {
avatarPath: string; avatarPath: string;
cardName: string; cardName: string;
cardType: number; cardType: number;
@ -43,7 +43,7 @@ export interface User {
export interface Group { export interface Group {
uid: string; // 群号 uid: string; // 群号
name: string; name: string;
members?: GroupMemberInfo[]; members?: GroupMember[];
} }
export interface Peer { export interface Peer {

4
src/global.d.ts vendored
View File

@ -1,7 +1,7 @@
import { import {
Config, Config,
Group, Group,
GroupMemberInfo, GroupMember,
MessageElement, MessageElement,
Peer, Peer,
PostDataSendMsg, PttElement, RawMessage, PostDataSendMsg, PttElement, RawMessage,
@ -27,7 +27,7 @@ declare var LLAPI: {
recallMessage(peer: Peer, msgIds: string[]): Promise<void>; recallMessage(peer: Peer, msgIds: string[]): Promise<void>;
getGroupsList(forced: boolean): Promise<Group[]> getGroupsList(forced: boolean): Promise<Group[]>
getFriendsList(forced: boolean): Promise<User[]> getFriendsList(forced: boolean): Promise<User[]>
getGroupMemberList(group_id: string, num: number): Promise<{result: { infos: Map<string, GroupMemberInfo> }}> getGroupMemberList(group_id: string, num: number): Promise<{result: { infos: Map<string, GroupMember> }}>
getPeer(): Promise<Peer> getPeer(): Promise<Peer>
add_qmenu(func: (qContextMenu: Node)=>void): void add_qmenu(func: (qContextMenu: Node)=>void): void
Ptt2Text(msgId:string, peer: Peer, elements: MessageElement[]): Promise<any> Ptt2Text(msgId:string, peer: Peer, elements: MessageElement[]): Promise<any>

View File

@ -22,7 +22,8 @@ import {checkFileReceived, CONFIG_DIR, file2base64, getConfigUtil, isGIF, log} f
import {friends, groups, msgHistory, selfInfo} from "../common/data"; import {friends, groups, msgHistory, selfInfo} from "../common/data";
import {} from "../global"; import {} from "../global";
import {hookNTQQApiReceive, ReceiveCmd, registerReceiveHook} from "../ntqqapi/hook"; import {hookNTQQApiReceive, ReceiveCmd, registerReceiveHook} from "../ntqqapi/hook";
import {OB11Construct} from "../onebot11/construct"; import {OB11Constructor} from "../onebot11/constructor";
import {NTQQApi} from "../ntqqapi/ntcall";
const fs = require('fs'); const fs = require('fs');
@ -174,7 +175,7 @@ function onLoad() {
function postRawMsg(msgList:RawMessage[]) { function postRawMsg(msgList:RawMessage[]) {
const {debug, reportSelfMessage} = getConfigUtil().getConfig(); const {debug, reportSelfMessage} = getConfigUtil().getConfig();
for (const message of msgList) { for (const message of msgList) {
OB11Construct.constructMessage(message).then((msg) => { OB11Constructor.message(message).then((msg) => {
if (debug) { if (debug) {
msg.raw = message; msg.raw = message;
} }
@ -206,6 +207,12 @@ function onLoad() {
log("report self message error: ", e.toString()) log("report self message error: ", e.toString())
} }
}) })
setTimeout(()=>{
NTQQApi.getSelfInfo().then(r=>{
log(r);
})
}, 10000)
} }

View File

@ -0,0 +1,98 @@
import {AtType} from "../common/types";
import {ElementType, SendPicElement, SendPttElement, SendReplyElement, SendTextElement} from "./types";
import {NTQQApi} from "./ntcall";
export class SendMsgElementConstructor {
static text(content: string): SendTextElement {
return {
elementType: ElementType.TEXT,
elementId: "",
textElement: {
content,
atType: AtType.notAt,
atUid: "",
atTinyId: "",
atNtUid: "",
},
};
}
static at(atUid: string, atNtUid: string, atType: AtType, atName: string): SendTextElement {
return {
elementType: ElementType.TEXT,
elementId: "",
textElement: {
content: `@${atName}`,
atType,
atUid,
atTinyId: "",
atNtUid,
},
};
}
reply(msgSeq: string, msgId: string, senderUin: string, senderUinStr: string): SendReplyElement {
return {
elementType: ElementType.REPLY,
elementId: "",
replyElement: {
replayMsgSeq: msgSeq, // raw.msgSeq
replayMsgId: msgId, // raw.msgId
senderUin: senderUin,
senderUinStr: senderUinStr,
}
}
}
async pic(picPath: string): Promise<SendPicElement>{
const {md5, fileName, path, fileSize} = await NTQQApi.uploadFile(picPath);
const imageSize = await NTQQApi.getImageSize(picPath);
const picElement = {
md5HexStr: md5,
fileSize: fileSize,
picWidth: imageSize.width,
picHeight: imageSize.height,
fileName: fileName,
sourcePath: path,
original: true,
picType: 1001,
picSubType: 0,
fileUuid: "",
fileSubId: "",
thumbFileSize: 0,
summary: "",
};
return {
elementType: ElementType.PIC,
elementId: "",
picElement
};
}
async ptt(pttPath: string):Promise<SendPttElement> {
const {md5, fileName, path, fileSize} = await NTQQApi.uploadFile(pttPath);
return {
elementType: ElementType.PTT,
elementId: "",
pttElement: {
fileName: fileName,
filePath: path,
md5HexStr: md5,
fileSize: fileSize,
duration: Math.max(1, Math.round(fileSize / 1024 / 3)), // 一秒钟大概是3kb大小, 小于1秒的按1秒算
formatType: 1,
voiceType: 1,
voiceChangeType: 0,
canConvert2Text: true,
waveAmplitudes: [
0, 18, 9, 23, 16, 17, 16, 15, 44, 17, 24, 20, 14, 15, 17,
],
fileSubId: "",
playState: 1,
autoConvertText: 0,
}
};
}
}

View File

@ -4,22 +4,25 @@ import {NTQQApiClass} from "./ntcall";
import {RawMessage} from "../common/types"; import {RawMessage} from "../common/types";
import {msgHistory} from "../common/data"; import {msgHistory} from "../common/data";
export let hookApiCallbacks: Record<string, (apiReturn: any)=>void>={}
export enum ReceiveCmd { export enum ReceiveCmd {
UPDATE_MSG = "nodeIKernelMsgListener/onMsgInfoListUpdate", UPDATE_MSG = "nodeIKernelMsgListener/onMsgInfoListUpdate",
NEW_MSG = "nodeIKernelMsgListener/onRecvMsg", NEW_MSG = "nodeIKernelMsgListener/onRecvMsg",
SELF_SEND_MSG = "nodeIKernelMsgListener/onAddSendMsg" SELF_SEND_MSG = "nodeIKernelMsgListener/onAddSendMsg"
} }
interface NTQQApiReturnData extends Array<any> { interface NTQQApiReturnData<PayloadType=unknown> extends Array<any> {
0: { 0: {
"type": "request", "type": "request",
"eventName": NTQQApiClass "eventName": NTQQApiClass,
"callbackId"?: string
}, },
1: 1:
{ {
cmdName: ReceiveCmd, cmdName: ReceiveCmd,
cmdType: "event", cmdType: "event",
payload: unknown payload: PayloadType
}[] }[]
} }
@ -35,15 +38,27 @@ export function hookNTQQApiReceive(window: BrowserWindow) {
if (args?.[1] instanceof Array) { if (args?.[1] instanceof Array) {
for (let receiveData of args?.[1]) { for (let receiveData of args?.[1]) {
const ntQQApiMethodName = receiveData.cmdName; const ntQQApiMethodName = receiveData.cmdName;
// log(`received ntqq api message: ${channel} ${ntQQApiMethodName}`, JSON.stringify(receiveData)) log(`received ntqq api message: ${channel} ${ntQQApiMethodName}`, JSON.stringify(receiveData))
for (let hook of receiveHooks) { for (let hook of receiveHooks) {
if (hook.method === ntQQApiMethodName) { if (hook.method === ntQQApiMethodName) {
new Promise((resolve, reject) => {
hook.hookFunc(receiveData.payload); hook.hookFunc(receiveData.payload);
}).then()
} }
} }
} }
} }
if (args[0]?.callbackId){
// log("hookApiCallback", hookApiCallbacks, args)
const callbackId = args[0].callbackId;
if (hookApiCallbacks[callbackId]){
// log("callback found")
new Promise((resolve, reject) => {
hookApiCallbacks[callbackId](args[1]);
}).then()
delete hookApiCallbacks[callbackId];
}
}
return originalSend.call(window.webContents, channel, ...args); return originalSend.call(window.webContents, channel, ...args);
} }
window.webContents.send = patchSend; window.webContents.send = patchSend;

View File

@ -1,5 +1,9 @@
import {ipcMain} from "electron"; import {ipcMain} from "electron";
import {v4 as uuidv4} from "uuid"; import {v4 as uuidv4} from "uuid";
import {hookApiCallbacks} from "./hook";
import {log} from "../common/utils";
import {ChatType, Group, GroupMember, User} from "../common/types";
import {SendMessageElement} from "./types";
interface IPCReceiveEvent { interface IPCReceiveEvent {
eventName: string eventName: string
@ -15,12 +19,27 @@ export type IPCReceiveDetail = [
export enum NTQQApiClass { export enum NTQQApiClass {
NT_API = "ns-ntApi", NT_API = "ns-ntApi",
NT_FS_API = "ns-FsApi", FS_API = "ns-FsApi",
GLOBAL_DATA = "ns-GlobalDataApi"
} }
export enum NTQQApiMethod { export enum NTQQApiMethod {
LIKE_FRIEND = "nodeIKernelProfileLikeService/setBuddyProfileLike", LIKE_FRIEND = "nodeIKernelProfileLikeService/setBuddyProfileLike",
UPDATE_MSG = "nodeIKernelMsgListener/onMsgInfoListUpdate" UPDATE_MSG = "nodeIKernelMsgListener/onMsgInfoListUpdate",
SELF_INFO = "fetchAuthData",
FRIENDS = "nodeIKernelProfileService/getBuddyProfileList",
GROUPS = "nodeIKernelGroupService/getGroupList",
GROUP_MEMBER_SCENE = "nodeIKernelGroupService/createMemberListScene",
GROUP_MEMBERS = "nodeIKernelGroupService/getNextMemberList",
USER_INFO = "nodeIKernelProfileService/getUserSimpleInfo",
FILE_TYPE = "getFileType",
FILE_MD5 = "getFileMd5",
FILE_COPY = "copyFile",
IMAGE_SIZE = "getImageSizeFromPath",
FILE_SIZE = "getFileSize",
MEDIA_FILE_PATH = "nodeIKernelMsgService/getRichMediaFilePathForGuild",
RECALL_MSG = "nodeIKernelMsgService/recallMsg",
SEND_MSG = "nodeIKernelMsgService/sendMsg",
} }
enum NTQQApiChannel { enum NTQQApiChannel {
@ -29,19 +48,22 @@ enum NTQQApiChannel {
IPC_UP_1 = "IPC_UP_1", IPC_UP_1 = "IPC_UP_1",
} }
interface Peer {
chatType: ChatType
peerUid: string // 是uid还是QQ号
guildId?: ""
}
function callNTQQApi(channel: NTQQApiChannel, className: NTQQApiClass, methodName: NTQQApiMethod, args: unknown[]=[]) {
function callNTQQApi<ReturnType>(channel: NTQQApiChannel, className: NTQQApiClass, methodName: NTQQApiMethod, args: unknown[] = []) {
const uuid = uuidv4(); const uuid = uuidv4();
return new Promise((resolve, reject) => { // log("callNTQQApi", channel, className, methodName, args, uuid)
return new Promise((resolve: (data: ReturnType) => void, reject) => {
// log("callNTQQApiPromise", channel, className, methodName, args, uuid)
hookApiCallbacks[uuid] = resolve;
ipcMain.emit( ipcMain.emit(
channel, channel,
{ {},
sender: {
send: (args: [string, IPCReceiveEvent, IPCReceiveDetail]) => {
resolve(args)
},
},
},
{type: 'request', callbackId: uuid, eventName: className + "-" + channel[channel.length - 1]}, {type: 'request', callbackId: uuid, eventName: className + "-" + channel[channel.length - 1]},
[methodName, ...args], [methodName, ...args],
) )
@ -62,4 +84,105 @@ export class NTQQApi {
}, },
null]) null])
} }
static getSelfInfo() {
return callNTQQApi<User>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.GLOBAL_DATA, NTQQApiMethod.SELF_INFO, [])
}
static getFriends(forced = false) {
return callNTQQApi(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.FRIENDS, [{force_update: forced}, undefined])
}
static getGroups(forced = false) {
return callNTQQApi<Group[]>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.GROUPS, [{force_update: forced}, undefined])
}
static async getGroupMembers(groupQQ: string, num = 3000) {
const sceneId = callNTQQApi(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.GROUP_MEMBER_SCENE, [{
groupCode: groupQQ,
scene: "groupMemberList_MainWindow"
}]
)
return callNTQQApi<GroupMember[]>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.GROUP_MEMBERS,
[{
sceneId: sceneId,
num: num
},
null
])
}
static async getUserInfo(uid: string) {
const result = await callNTQQApi<[{
payload: { profiles: Map<string, User> }
}]>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.USER_INFO,
[{force: true, uids: [uid]}, undefined])
return new Promise<User>(resolve => {
resolve(result[0].payload.profiles.get(uid))
})
}
static getFileType(filePath: string) {
return callNTQQApi<{
ext: string
}>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.FS_API, NTQQApiMethod.FILE_TYPE, [filePath])
}
static getFileMd5(filePath: string) {
return callNTQQApi<string>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.FS_API, NTQQApiMethod.FILE_MD5, [filePath])
}
static copyFile(filePath: string, destPath: string) {
return callNTQQApi<string>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.FS_API, NTQQApiMethod.FILE_COPY, [filePath, destPath])
}
static getImageSize(filePath: string) {
return callNTQQApi<{
width: number,
height: number
}>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.FS_API, NTQQApiMethod.IMAGE_SIZE, [filePath])
}
static getFileSize(filePath: string) {
return callNTQQApi<number>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.FS_API, NTQQApiMethod.FILE_SIZE, [filePath])
}
// 上传文件到QQ的文件夹
static async uploadFile(filePath: string) {
const md5 = await NTQQApi.getFileMd5(filePath);
const fileName = `${md5}.${(await NTQQApi.getFileType(filePath)).ext}`;
const mediaPath = await callNTQQApi<string>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.FS_API, NTQQApiMethod.MEDIA_FILE_PATH, [{
path_info: {
md5HexStr: md5,
fileName: fileName,
elementType: 2,
elementSubType: 0,
thumbSize: 0,
needCreate: true,
downloadType: 1,
file_uuid: ""
}
}])
await NTQQApi.copyFile(filePath, mediaPath);
const fileSize = await NTQQApi.getFileSize(filePath);
return {
md5,
fileName,
path: mediaPath,
fileSize
}
}
static recallMsg(peer: Peer, msgIds: string[]){
return callNTQQApi(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.RECALL_MSG, [{peer, msgIds}, null])
}
static sendMsg(peer: Peer, msgElements: SendMessageElement){
return callNTQQApi(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.SEND_MSG, [{
msgId: "0",
peer, msgElements,
msgAttributeInfos: new Map(),
}, null])
}
} }

70
src/ntqqapi/types.ts Normal file
View File

@ -0,0 +1,70 @@
export enum ElementType {
TEXT = 1,
PIC = 2,
PTT = 4,
REPLY = 7,
}
export interface SendTextElement {
elementType: ElementType.TEXT,
elementId: "",
textElement: {
content: string,
atType: number,
atUid: string,
atTinyId: string,
atNtUid: string,
}
}
export interface SendPttElement {
elementType: ElementType.PTT,
elementId: "",
pttElement: {
fileName: string,
filePath: string,
md5HexStr: string,
fileSize: number,
duration: number,
formatType: number,
voiceType: number,
voiceChangeType: number,
canConvert2Text: boolean,
waveAmplitudes: number[],
fileSubId: "",
playState: number,
autoConvertText: number,
}
}
export interface SendPicElement {
elementType: ElementType.PIC,
elementId: "",
picElement: {
md5HexStr: string,
fileSize: number,
picWidth: number,
picHeight: number,
fileName: string,
sourcePath: string,
original: boolean,
picType: number,
picSubType: number,
fileUuid: string,
fileSubId: string,
thumbFileSize: number,
summary: string,
}
}
export interface SendReplyElement {
elementType: ElementType.REPLY,
elementId: "",
replyElement: {
replayMsgSeq: string,
replayMsgId: string,
senderUin: string,
senderUinStr: string,
}
}
export type SendMessageElement = SendTextElement | SendPttElement | SendPicElement | SendReplyElement

View File

@ -4,8 +4,8 @@ import {getFriend, getGroupMember, getHistoryMsgBySeq, msgHistory, selfInfo} fro
import {file2base64, getConfigUtil} from "../common/utils"; import {file2base64, getConfigUtil} from "../common/utils";
export class OB11Construct { export class OB11Constructor {
static async constructMessage(msg: RawMessage): Promise<OB11Message> { static async message(msg: RawMessage): Promise<OB11Message> {
const {enableBase64} = getConfigUtil().getConfig() const {enableBase64} = getConfigUtil().getConfig()
const message_type = msg.chatType == ChatType.group ? "group" : "private"; const message_type = msg.chatType == ChatType.group ? "group" : "private";
const resMsg: OB11Message = { const resMsg: OB11Message = {
@ -30,7 +30,7 @@ export class OB11Construct {
resMsg.group_id = msg.peerUin resMsg.group_id = msg.peerUin
const member = getGroupMember(msg.peerUin, msg.senderUin); const member = getGroupMember(msg.peerUin, msg.senderUin);
if (member) { if (member) {
resMsg.sender.role = OB11Construct.constructGroupMemberRole(member.role); resMsg.sender.role = OB11Constructor.groupMemberRole(member.role);
} }
} else if (msg.chatType == ChatType.friend) { } else if (msg.chatType == ChatType.friend) {
resMsg.sub_type = "friend" resMsg.sub_type = "friend"
@ -119,7 +119,7 @@ export class OB11Construct {
return resMsg; return resMsg;
} }
static constructGroupMemberRole(role: number): OB11GroupMemberRole { static groupMemberRole(role: number): OB11GroupMemberRole {
return { return {
4: OB11GroupMemberRole.owner, 4: OB11GroupMemberRole.owner,
3: OB11GroupMemberRole.admin, 3: OB11GroupMemberRole.admin,

View File

@ -10,7 +10,7 @@ import {sendIPCRecallQQMsg, sendIPCSendQQMsg} from "../main/ipcsend";
import {PostDataSendMsg} from "../common/types"; import {PostDataSendMsg} from "../common/types";
import {friends, groups, msgHistory, selfInfo} from "../common/data"; import {friends, groups, msgHistory, selfInfo} from "../common/data";
import {OB11ApiName, OB11Message, OB11Return, OB11MessageData} from "../onebot11/types"; import {OB11ApiName, OB11Message, OB11Return, OB11MessageData} from "../onebot11/types";
import {OB11Construct} from "../onebot11/construct"; import {OB11Constructor} from "../onebot11/constructor";
// @SiberianHusky 2021-08-15 // @SiberianHusky 2021-08-15
@ -126,7 +126,7 @@ function handlePost(jsonData: any, handleSendResult: (data: OB11Return<any>) =>
user_display_name: member.cardName || member.nick, user_display_name: member.cardName || member.nick,
nickname: member.nick, nickname: member.nick,
card: member.cardName, card: member.cardName,
role: OB11Construct.constructGroupMemberRole(member.role), role: OB11Constructor.groupMemberRole(member.role),
} }
} else if (jsonData.action == "get_group_member_list") { } else if (jsonData.action == "get_group_member_list") {
let group = groups.find(group => group.uid == jsonData.params.group_id) let group = groups.find(group => group.uid == jsonData.params.group_id)
@ -138,7 +138,7 @@ function handlePost(jsonData: any, handleSendResult: (data: OB11Return<any>) =>
user_display_name: member.cardName || member.nick, user_display_name: member.cardName || member.nick,
nickname: member.nick, nickname: member.nick,
card: member.cardName, card: member.cardName,
role: OB11Construct.constructGroupMemberRole(member.role), role: OB11Constructor.groupMemberRole(member.role),
} }
}) || [] }) || []
@ -251,7 +251,7 @@ export function startExpress(port: number) {
registerRouter<{ message_id: string }, OB11Message>("get_msg", async (payload) => { registerRouter<{ message_id: string }, OB11Message>("get_msg", async (payload) => {
const msg = msgHistory[payload.message_id.toString()] const msg = msgHistory[payload.message_id.toString()]
if (msg) { if (msg) {
const msgData = await OB11Construct.constructMessage(msg); const msgData = await OB11Constructor.message(msg);
return constructReturnData(0, msgData) return constructReturnData(0, msgData)
} else { } else {
return constructReturnData(1, {}, "消息不存在") return constructReturnData(1, {}, "消息不存在")