diff --git a/src/ntqqapi/hook.ts b/src/ntqqapi/hook.ts index b189e6f..0233f14 100644 --- a/src/ntqqapi/hook.ts +++ b/src/ntqqapi/hook.ts @@ -1,72 +1,73 @@ -import {type BrowserWindow} from 'electron' -import {getConfigUtil, log, sleep} from '../common/utils' -import {NTQQApi, type NTQQApiClass, sendMessagePool} from './ntcall' -import {type Group, type RawMessage, type User} from './types' -import {friends, groups, selfInfo, tempGroupCodeMap} from '../common/data' -import {OB11GroupDecreaseEvent} from '../onebot11/event/notice/OB11GroupDecreaseEvent' -import {OB11GroupIncreaseEvent} from '../onebot11/event/notice/OB11GroupIncreaseEvent' -import {v4 as uuidv4} from 'uuid' -import {postOB11Event} from '../onebot11/server/postOB11Event' -import {HOOK_LOG} from '../common/config' -import fs from 'fs' +import {BrowserWindow} from 'electron'; +import {getConfigUtil, log, sleep} from "../common/utils"; +import {NTQQApi, NTQQApiClass, sendMessagePool} from "./ntcall"; +import {Group, RawMessage, User} from "./types"; +import {friends, groups, selfInfo, tempGroupCodeMap} from "../common/data"; +import {OB11GroupDecreaseEvent} from "../onebot11/event/notice/OB11GroupDecreaseEvent"; +import {OB11GroupIncreaseEvent} from "../onebot11/event/notice/OB11GroupIncreaseEvent"; +import {v4 as uuidv4} from "uuid" +import {postOB11Event} from "../onebot11/server/postOB11Event"; +import {HOOK_LOG} from "../common/config"; +import fs from "fs"; import {dbUtil} from "../common/db"; -export const hookApiCallbacks: Record void> = {} +export let hookApiCallbacks: Record void> = {} export enum ReceiveCmd { - UPDATE_MSG = 'nodeIKernelMsgListener/onMsgInfoListUpdate', - NEW_MSG = 'nodeIKernelMsgListener/onRecvMsg', - SELF_SEND_MSG = 'nodeIKernelMsgListener/onAddSendMsg', - USER_INFO = 'nodeIKernelProfileListener/onProfileSimpleChanged', - USER_DETAIL_INFO = 'nodeIKernelProfileListener/onProfileDetailInfoChanged', - GROUPS = 'nodeIKernelGroupListener/onGroupListUpdate', - GROUPS_UNIX = 'onGroupListUpdate', - FRIENDS = 'onBuddyListChange', - MEDIA_DOWNLOAD_COMPLETE = 'nodeIKernelMsgListener/onRichMediaDownloadComplete', - UNREAD_GROUP_NOTIFY = 'nodeIKernelGroupListener/onGroupNotifiesUnreadCountUpdated', - GROUP_NOTIFY = 'nodeIKernelGroupListener/onGroupSingleScreenNotifies', - FRIEND_REQUEST = 'nodeIKernelBuddyListener/onBuddyReqChange', + UPDATE_MSG = "nodeIKernelMsgListener/onMsgInfoListUpdate", + NEW_MSG = "nodeIKernelMsgListener/onRecvMsg", + SELF_SEND_MSG = "nodeIKernelMsgListener/onAddSendMsg", + USER_INFO = "nodeIKernelProfileListener/onProfileSimpleChanged", + USER_DETAIL_INFO = "nodeIKernelProfileListener/onProfileDetailInfoChanged", + GROUPS = "nodeIKernelGroupListener/onGroupListUpdate", + GROUPS_UNIX = "onGroupListUpdate", + FRIENDS = "onBuddyListChange", + MEDIA_DOWNLOAD_COMPLETE = "nodeIKernelMsgListener/onRichMediaDownloadComplete", + UNREAD_GROUP_NOTIFY = "nodeIKernelGroupListener/onGroupNotifiesUnreadCountUpdated", + GROUP_NOTIFY = "nodeIKernelGroupListener/onGroupSingleScreenNotifies", + FRIEND_REQUEST = "nodeIKernelBuddyListener/onBuddyReqChange", SELF_STATUS = 'nodeIKernelProfileListener/onSelfStatusChanged', + CACHE_SCAN_FINISH = "nodeIKernelStorageCleanListener/onFinishScan", } interface NTQQApiReturnData extends Array { 0: { - 'type': 'request' - 'eventName': NTQQApiClass - 'callbackId'?: string - } + "type": "request", + "eventName": NTQQApiClass, + "callbackId"?: string + }, 1: - Array<{ - cmdName: ReceiveCmd - cmdType: 'event' + { + cmdName: ReceiveCmd, + cmdType: "event", payload: PayloadType - }> + }[] } -const receiveHooks: Array<{ - method: ReceiveCmd +let receiveHooks: Array<{ + method: ReceiveCmd, hookFunc: ((payload: any) => void | Promise) id: string }> = [] export function hookNTQQApiReceive(window: BrowserWindow) { - const originalSend = window.webContents.send + const originalSend = window.webContents.send; const patchSend = (channel: string, ...args: NTQQApiReturnData) => { HOOK_LOG && log(`received ntqq api message: ${channel}`, JSON.stringify(args)) if (args?.[1] instanceof Array) { - for (const receiveData of args?.[1]) { - const ntQQApiMethodName = receiveData.cmdName + for (let receiveData of args?.[1]) { + const ntQQApiMethodName = receiveData.cmdName; // log(`received ntqq api message: ${channel} ${ntQQApiMethodName}`, JSON.stringify(receiveData)) - for (const hook of receiveHooks) { + for (let hook of receiveHooks) { if (hook.method === ntQQApiMethodName) { new Promise((resolve, reject) => { try { - const _ = hook.hookFunc(receiveData.payload) - if (hook.hookFunc.constructor.name === 'AsyncFunction') { + let _ = hook.hookFunc(receiveData.payload) + if (hook.hookFunc.constructor.name === "AsyncFunction") { (_ as Promise).then() } } catch (e) { - log('hook error', e, receiveData.payload) + log("hook error", e, receiveData.payload) } }).then() } @@ -75,35 +76,35 @@ export function hookNTQQApiReceive(window: BrowserWindow) { } if (args[0]?.callbackId) { // log("hookApiCallback", hookApiCallbacks, args) - const callbackId = args[0].callbackId + const callbackId = args[0].callbackId; if (hookApiCallbacks[callbackId]) { // log("callback found") new Promise((resolve, reject) => { - hookApiCallbacks[callbackId](args[1]) + hookApiCallbacks[callbackId](args[1]); }).then() - delete hookApiCallbacks[callbackId] + 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; } export function hookNTQQApiCall(window: BrowserWindow) { // 监听调用NTQQApi - const webContents = window.webContents as any - const ipc_message_proxy = webContents._events['-ipc-message']?.[0] || webContents._events['-ipc-message'] + let webContents = window.webContents as any; + const ipc_message_proxy = webContents._events["-ipc-message"]?.[0] || webContents._events["-ipc-message"]; const proxyIpcMsg = new Proxy(ipc_message_proxy, { apply(target, thisArg, args) { - HOOK_LOG && log('call NTQQ api', thisArg, args) - return target.apply(thisArg, args) - } - }) - if (webContents._events['-ipc-message']?.[0]) { - webContents._events['-ipc-message'][0] = proxyIpcMsg + HOOK_LOG && log("call NTQQ api", thisArg, args); + return target.apply(thisArg, args); + }, + }); + if (webContents._events["-ipc-message"]?.[0]) { + webContents._events["-ipc-message"][0] = proxyIpcMsg; } else { - webContents._events['-ipc-message'] = proxyIpcMsg + webContents._events["-ipc-message"] = proxyIpcMsg; } } @@ -114,29 +115,29 @@ export function registerReceiveHook(method: ReceiveCmd, hookFunc: ( hookFunc, id }) - return id + return id; } export function removeReceiveHook(id: string) { const index = receiveHooks.findIndex(h => h.id === id) - receiveHooks.splice(index, 1) + receiveHooks.splice(index, 1); } async function updateGroups(_groups: Group[], needUpdate: boolean = true) { - for (const group of _groups) { - let existGroup = groups.find(g => g.groupCode == group.groupCode) + for (let group of _groups) { + let existGroup = groups.find(g => g.groupCode == group.groupCode); if (existGroup) { - Object.assign(existGroup, group) + Object.assign(existGroup, group); } else { - groups.push(group) - existGroup = group + groups.push(group); + existGroup = group; } if (needUpdate) { - const members = await NTQQApi.getGroupMembers(group.groupCode) + const members = await NTQQApi.getGroupMembers(group.groupCode); if (members) { - existGroup.members = members + existGroup.members = members; } } } @@ -144,83 +145,84 @@ async function updateGroups(_groups: Group[], needUpdate: boolean = true) { async function processGroupEvent(payload) { try { - const newGroupList = payload.groupList + const newGroupList = payload.groupList; for (const group of newGroupList) { - const existGroup = groups.find(g => g.groupCode == group.groupCode) + let existGroup = groups.find(g => g.groupCode == group.groupCode); if (existGroup) { if (existGroup.memberCount > group.memberCount) { - const oldMembers = existGroup.members + const oldMembers = existGroup.members; - await sleep(200) // 如果请求QQ API的速度过快,通常无法正确拉取到最新的群信息,因此这里人为引入一个延时 - const newMembers = await NTQQApi.getGroupMembers(group.groupCode) + await sleep(200); // 如果请求QQ API的速度过快,通常无法正确拉取到最新的群信息,因此这里人为引入一个延时 + const newMembers = await NTQQApi.getGroupMembers(group.groupCode); - group.members = newMembers - const newMembersSet = new Set() // 建立索引降低时间复杂度 + group.members = newMembers; + const newMembersSet = new Set(); // 建立索引降低时间复杂度 for (const member of newMembers) { - newMembersSet.add(member.uin) + newMembersSet.add(member.uin); } for (const member of oldMembers) { if (!newMembersSet.has(member.uin)) { - postOB11Event(new OB11GroupDecreaseEvent(group.groupCode, parseInt(member.uin))) - break + postOB11Event(new OB11GroupDecreaseEvent(group.groupCode, parseInt(member.uin))); + break; } } + } else if (existGroup.memberCount < group.memberCount) { - const oldMembers = existGroup.members - const oldMembersSet = new Set() + const oldMembers = existGroup.members; + const oldMembersSet = new Set(); for (const member of oldMembers) { - oldMembersSet.add(member.uin) + oldMembersSet.add(member.uin); } - await sleep(200) - const newMembers = await NTQQApi.getGroupMembers(group.groupCode) + await sleep(200); + const newMembers = await NTQQApi.getGroupMembers(group.groupCode); - group.members = newMembers + group.members = newMembers; for (const member of newMembers) { if (!oldMembersSet.has(member.uin)) { - postOB11Event(new OB11GroupIncreaseEvent(group.groupCode, parseInt(member.uin))) - break + postOB11Event(new OB11GroupIncreaseEvent(group.groupCode, parseInt(member.uin))); + break; } } } } } - updateGroups(newGroupList, false).then() + updateGroups(newGroupList, false).then(); } catch (e) { - updateGroups(payload.groupList).then() - console.log(e) + updateGroups(payload.groupList).then(); + console.log(e); } } registerReceiveHook<{ groupList: Group[], updateType: number }>(ReceiveCmd.GROUPS, (payload) => { if (payload.updateType != 2) { - updateGroups(payload.groupList).then() + updateGroups(payload.groupList).then(); } else { - if (process.platform == 'win32') { - processGroupEvent(payload).then() + if (process.platform == "win32") { + processGroupEvent(payload).then(); } } }) registerReceiveHook<{ groupList: Group[], updateType: number }>(ReceiveCmd.GROUPS_UNIX, (payload) => { if (payload.updateType != 2) { - updateGroups(payload.groupList).then() + updateGroups(payload.groupList).then(); } else { - if (process.platform != 'win32') { - processGroupEvent(payload).then() + if (process.platform != "win32") { + processGroupEvent(payload).then(); } } }) registerReceiveHook<{ - data: Array<{ categoryId: number, categroyName: string, categroyMbCount: number, buddyList: User[] }> + data: { categoryId: number, categroyName: string, categroyMbCount: number, buddyList: User[] }[] }>(ReceiveCmd.FRIENDS, payload => { for (const fData of payload.data) { - const _friends = fData.buddyList - for (const friend of _friends) { - const existFriend = friends.find(f => f.uin == friend.uin) + const _friends = fData.buddyList; + for (let friend of _friends) { + let existFriend = friends.find(f => f.uin == friend.uin) if (!existFriend) { friends.push(friend) } else { @@ -230,8 +232,8 @@ registerReceiveHook<{ } }) -registerReceiveHook<{ msgList: RawMessage[] }>(ReceiveCmd.NEW_MSG, (payload) => { - const {autoDeleteFile, autoDeleteFileSecond} = getConfigUtil().getConfig() +registerReceiveHook<{ msgList: Array }>(ReceiveCmd.NEW_MSG, (payload) => { + const {autoDeleteFile} = getConfigUtil().getConfig(); for (const message of payload.msgList) { // log("收到新消息,push到历史记录", message.msgSeq) dbUtil.addMsg(message).then() @@ -255,27 +257,27 @@ registerReceiveHook<{ msgList: RawMessage[] }>(ReceiveCmd.NEW_MSG, (payload) => for (const path of pathList) { if (path) { fs.unlink(picPath, () => { - log('删除文件成功', path) - }) + log("删除文件成功", path) + }); } } - }, autoDeleteFileSecond * 1000) + }, 60 * 1000) } } }) registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmd.SELF_SEND_MSG, ({msgRecord}) => { - const message = msgRecord - const peerUid = message.peerUid + const message = msgRecord; + const peerUid = message.peerUid; // log("收到自己发送成功的消息", Object.keys(sendMessagePool), message); // log("收到自己发送成功的消息", message); dbUtil.addMsg(message).then() const sendCallback = sendMessagePool[peerUid] if (sendCallback) { try { - sendCallback(message) + sendCallback(message); } catch (e) { - log('receive self msg error', e.stack) + log("receive self msg error", e.stack) } } }) diff --git a/src/ntqqapi/ntcall.ts b/src/ntqqapi/ntcall.ts index 16c6f1d..a838867 100644 --- a/src/ntqqapi/ntcall.ts +++ b/src/ntqqapi/ntcall.ts @@ -1,25 +1,29 @@ -import {ipcMain} from 'electron' -import {hookApiCallbacks, ReceiveCmd, registerReceiveHook, removeReceiveHook} from './hook' -import {log, sleep} from '../common/utils' +import {ipcMain} from "electron"; +import {hookApiCallbacks, ReceiveCmd, registerReceiveHook, removeReceiveHook} from "./hook"; +import {log, sleep} from "../common/utils"; import { - type ChatType, + ChatType, ElementType, - type Friend, - type FriendRequest, - type Group, GroupMember, - type GroupMemberRole, - type GroupNotifies, - type GroupNotify, - type GroupRequestOperateTypes, - type RawMessage, - type SelfInfo, - type SendMessageElement, - type User -} from './types' -import * as fs from 'node:fs' -import {friendRequests, groupNotifies, selfInfo, uidMaps} from '../common/data' -import {v4 as uuidv4} from 'uuid' -import path from 'path' + Friend, + FriendRequest, + Group, + GroupMember, + GroupMemberRole, + GroupNotifies, + GroupNotify, + GroupRequestOperateTypes, + RawMessage, + SelfInfo, + SendMessageElement, + User, + CacheScanResult, + ChatCacheList, ChatCacheListItemBasic, + CacheFileList, CacheFileListItem, CacheFileType, +} from "./types"; +import * as fs from "fs"; +import {friendRequests, groupNotifies, selfInfo, uidMaps} from "../common/data"; +import {v4 as uuidv4} from "uuid" +import path from "path"; import {dbUtil} from "../common/db"; interface IPCReceiveEvent { @@ -35,89 +39,104 @@ export type IPCReceiveDetail = [ ] export enum NTQQApiClass { - NT_API = 'ns-ntApi', - FS_API = 'ns-FsApi', - GLOBAL_DATA = 'ns-GlobalDataApi' + NT_API = "ns-ntApi", + FS_API = "ns-FsApi", + OS_API = "ns-OsApi", + HOTUPDATE_API = "ns-HotUpdateApi", + BUSINESS_API = "ns-BusinessApi", + GLOBAL_DATA = "ns-GlobalDataApi" } export enum NTQQApiMethod { - LIKE_FRIEND = 'nodeIKernelProfileLikeService/setBuddyProfileLike', - SELF_INFO = 'fetchAuthData', - FRIENDS = 'nodeIKernelBuddyService/getBuddyList', - GROUPS = 'nodeIKernelGroupService/getGroupList', - GROUP_MEMBER_SCENE = 'nodeIKernelGroupService/createMemberListScene', - GROUP_MEMBERS = 'nodeIKernelGroupService/getNextMemberList', - USER_INFO = 'nodeIKernelProfileService/getUserSimpleInfo', - USER_DETAIL_INFO = 'nodeIKernelProfileService/getUserDetailInfo', - 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', - DOWNLOAD_MEDIA = 'nodeIKernelMsgService/downloadRichMedia', - FORWARD_MSG = "nodeIKernelMsgService/forwardMsgWithComment", // 逐条转发 - MULTI_FORWARD_MSG = 'nodeIKernelMsgService/multiForwardMsgWithComment', // 合并转发 - GET_GROUP_NOTICE = 'nodeIKernelGroupService/getSingleScreenNotifies', - HANDLE_GROUP_REQUEST = 'nodeIKernelGroupService/operateSysNotify', - QUIT_GROUP = 'nodeIKernelGroupService/quitGroup', + LIKE_FRIEND = "nodeIKernelProfileLikeService/setBuddyProfileLike", + SELF_INFO = "fetchAuthData", + FRIENDS = "nodeIKernelBuddyService/getBuddyList", + GROUPS = "nodeIKernelGroupService/getGroupList", + GROUP_MEMBER_SCENE = "nodeIKernelGroupService/createMemberListScene", + GROUP_MEMBERS = "nodeIKernelGroupService/getNextMemberList", + USER_INFO = "nodeIKernelProfileService/getUserSimpleInfo", + USER_DETAIL_INFO = "nodeIKernelProfileService/getUserDetailInfo", + 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", + DOWNLOAD_MEDIA = "nodeIKernelMsgService/downloadRichMedia", + FORWARD_MSG = "nodeIKernelMsgService/forwardMsgWithComment", + MULTI_FORWARD_MSG = "nodeIKernelMsgService/multiForwardMsgWithComment", // 合并转发 + GET_GROUP_NOTICE = "nodeIKernelGroupService/getSingleScreenNotifies", + HANDLE_GROUP_REQUEST = "nodeIKernelGroupService/operateSysNotify", + QUIT_GROUP = "nodeIKernelGroupService/quitGroup", // READ_FRIEND_REQUEST = "nodeIKernelBuddyListener/onDoubtBuddyReqUnreadNumChange" - HANDLE_FRIEND_REQUEST = 'nodeIKernelBuddyService/approvalFriendRequest', - KICK_MEMBER = 'nodeIKernelGroupService/kickMember', - MUTE_MEMBER = 'nodeIKernelGroupService/setMemberShutUp', - MUTE_GROUP = 'nodeIKernelGroupService/setGroupShutUp', - SET_MEMBER_CARD = 'nodeIKernelGroupService/modifyMemberCardName', - SET_MEMBER_ROLE = 'nodeIKernelGroupService/modifyMemberRole', - PUBLISH_GROUP_BULLETIN = 'nodeIKernelGroupService/publishGroupBulletinBulletin', - SET_GROUP_NAME = 'nodeIKernelGroupService/modifyGroupName', + HANDLE_FRIEND_REQUEST = "nodeIKernelBuddyService/approvalFriendRequest", + KICK_MEMBER = "nodeIKernelGroupService/kickMember", + MUTE_MEMBER = "nodeIKernelGroupService/setMemberShutUp", + MUTE_GROUP = "nodeIKernelGroupService/setGroupShutUp", + SET_MEMBER_CARD = "nodeIKernelGroupService/modifyMemberCardName", + SET_MEMBER_ROLE = "nodeIKernelGroupService/modifyMemberRole", + PUBLISH_GROUP_BULLETIN = "nodeIKernelGroupService/publishGroupBulletinBulletin", + SET_GROUP_NAME = "nodeIKernelGroupService/modifyGroupName", + + CACHE_SET_SILENCE = 'nodeIKernelStorageCleanService/setSilentScan', + CACHE_ADD_SCANNED_PATH = 'nodeIKernelStorageCleanService/addCacheScanedPaths', + CACHE_PATH_HOT_UPDATE = 'getHotUpdateCachePath', + CACHE_PATH_DESKTOP_TEMP = 'getDesktopTmpPath', + CACHE_PATH_SESSION = 'getCleanableAppSessionPathList', + CACHE_SCAN = 'nodeIKernelStorageCleanService/scanCache', + CACHE_CLEAR = 'nodeIKernelStorageCleanService/clearCacheDataByKeys', + + CACHE_CHAT_GET = 'nodeIKernelStorageCleanService/getChatCacheInfo', + CACHE_FILE_GET = 'nodeIKernelStorageCleanService/getFileCacheInfo', + CACHE_CHAT_CLEAR = 'nodeIKernelStorageCleanService/clearChatCacheInfo', } enum NTQQApiChannel { - IPC_UP_2 = 'IPC_UP_2', - IPC_UP_3 = 'IPC_UP_3', - IPC_UP_1 = 'IPC_UP_1', + IPC_UP_2 = "IPC_UP_2", + IPC_UP_3 = "IPC_UP_3", + IPC_UP_1 = "IPC_UP_1", } export interface Peer { chatType: ChatType - peerUid: string // 如果是群聊uid为群号,私聊uid就是加密的字符串 - guildId?: '' + peerUid: string // 如果是群聊uid为群号,私聊uid就是加密的字符串 + guildId?: "" } interface NTQQApiParams { - methodName: NTQQApiMethod | string - className?: NTQQApiClass - channel?: NTQQApiChannel + methodName: NTQQApiMethod | string, + className?: NTQQApiClass, + channel?: NTQQApiChannel, classNameIsRegister?: boolean - args?: unknown[] - cbCmd?: ReceiveCmd | null - cmdCB?: (payload: any) => boolean - afterFirstCmd?: boolean // 是否在methodName调用完之后再去hook cbCmd - timeoutSecond?: number + args?: unknown[], + cbCmd?: ReceiveCmd | null, + cmdCB?: (payload: any) => boolean; + afterFirstCmd?: boolean, // 是否在methodName调用完之后再去hook cbCmd + timeoutSecond?: number, } -async function callNTQQApi(params: NTQQApiParams) { +function callNTQQApi(params: NTQQApiParams) { let { className, methodName, channel, args, cbCmd, timeoutSecond: timeout, classNameIsRegister, cmdCB, afterFirstCmd - } = params - className = className ?? NTQQApiClass.NT_API - channel = channel ?? NTQQApiChannel.IPC_UP_2 - args = args ?? [] - timeout = timeout ?? 5 - afterFirstCmd = afterFirstCmd ?? true - const uuid = uuidv4() + } = params; + className = className ?? NTQQApiClass.NT_API; + channel = channel ?? NTQQApiChannel.IPC_UP_2; + args = args ?? []; + timeout = timeout ?? 5; + afterFirstCmd = afterFirstCmd ?? true; + const uuid = uuidv4(); // log("callNTQQApi", channel, className, methodName, args, uuid) - return await new Promise((resolve: (data: ReturnType) => void, reject) => { + return new Promise((resolve: (data: ReturnType) => void, reject) => { // log("callNTQQApiPromise", channel, className, methodName, args, uuid) const _timeout = timeout * 1000 let success = false - let eventName = className + '-' + channel[channel.length - 1] + let eventName = className + "-" + channel[channel.length - 1]; if (classNameIsRegister) { - eventName += '-register' + eventName += "-register"; } const apiArgs = [methodName, ...args] if (!cbCmd) { @@ -125,40 +144,40 @@ async function callNTQQApi(params: NTQQApiParams) { hookApiCallbacks[uuid] = (r: ReturnType) => { success = true resolve(r) - } + }; } else { // 这里的callback比较特殊,QQ后端先返回是否调用成功,再返回一条结果数据 const secondCallback = () => { const hookId = registerReceiveHook(cbCmd, (payload) => { // log(methodName, "second callback", cbCmd, payload, cmdCB); - if (cmdCB) { + if (!!cmdCB) { if (cmdCB(payload)) { - removeReceiveHook(hookId) + removeReceiveHook(hookId); success = true - resolve(payload) + resolve(payload); } } else { - removeReceiveHook(hookId) + removeReceiveHook(hookId); success = true - resolve(payload) + resolve(payload); } }) } - !afterFirstCmd && secondCallback() + !afterFirstCmd && secondCallback(); hookApiCallbacks[uuid] = (result: GeneralCallResult) => { log(`${methodName} callback`, result) if (result?.result == 0 || result === undefined) { - afterFirstCmd && secondCallback() + afterFirstCmd && secondCallback(); } else { success = true - reject(`ntqq api call failed, ${result.errMsg}`) + reject(`ntqq api call failed, ${result.errMsg}`); } } } setTimeout(() => { // log("ntqq api timeout", success, channel, className, methodName) if (!success) { - log(`ntqq api timeout ${channel}, ${eventName}, ${methodName}`, apiArgs) + log(`ntqq api timeout ${channel}, ${eventName}, ${methodName}`, apiArgs); reject(`ntqq api timeout ${channel}, ${eventName}, ${methodName}, ${apiArgs}`) } }, _timeout) @@ -172,13 +191,15 @@ async function callNTQQApi(params: NTQQApiParams) { }) } -export const sendMessagePool: Record void) | null> = {}// peerUid: callbackFunnc + +export let sendMessagePool: Record void) | null> = {}// peerUid: callbackFunnc interface GeneralCallResult { - result: number // 0: success + result: number, // 0: success errMsg: string } + export class NTQQApi { // static likeFriend = defineNTQQApi(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.LIKE_FRIEND) static async likeFriend(uid: string, count = 1) { @@ -198,9 +219,7 @@ export class NTQQApi { static async getSelfInfo() { return await callNTQQApi({ className: NTQQApiClass.GLOBAL_DATA, - // channel: NTQQApiChannel.IPC_UP_3, - methodName: NTQQApiMethod.SELF_INFO, - timeoutSecond: 2 + methodName: NTQQApiMethod.SELF_INFO, timeoutSecond: 2 }) } @@ -235,19 +254,19 @@ export class NTQQApi { static async getFriends(forced = false) { const data = await callNTQQApi<{ - data: Array<{ - categoryId: number - categroyName: string - categroyMbCount: number + data: { + categoryId: number, + categroyName: string, + categroyMbCount: number, buddyList: Friend[] - }> + }[] }>( { methodName: NTQQApiMethod.FRIENDS, args: [{force_update: forced}, undefined], cbCmd: ReceiveCmd.FRIENDS }) - const _friends: Friend[] = [] + let _friends: Friend[] = []; for (const fData of data.data) { _friends.push(...fData.buddyList) } @@ -256,11 +275,11 @@ export class NTQQApi { static async getGroups(forced = false) { let cbCmd = ReceiveCmd.GROUPS - if (process.platform != 'win32') { + if (process.platform != "win32") { cbCmd = ReceiveCmd.GROUPS_UNIX } const result = await callNTQQApi<{ - updateType: number + updateType: number, groupList: Group[] }>({methodName: NTQQApiMethod.GROUPS, args: [{force_update: forced}, undefined], cbCmd}) return result.groupList @@ -271,7 +290,7 @@ export class NTQQApi { methodName: NTQQApiMethod.GROUP_MEMBER_SCENE, args: [{ groupCode: groupQQ, - scene: 'groupMemberList_MainWindow' + scene: "groupMemberList_MainWindow" }] }) // log("get group member sceneId", sceneId); @@ -281,8 +300,8 @@ export class NTQQApi { }>({ methodName: NTQQApiMethod.GROUP_MEMBERS, args: [{ - sceneId, - num + sceneId: sceneId, + num: num }, null ] @@ -343,35 +362,35 @@ export class NTQQApi { // 上传文件到QQ的文件夹 static async uploadFile(filePath: string, elementType: ElementType = ElementType.PIC) { - const md5 = await NTQQApi.getFileMd5(filePath) + const md5 = await NTQQApi.getFileMd5(filePath); let ext = (await NTQQApi.getFileType(filePath))?.ext if (ext) { - ext = '.' + ext + ext = "." + ext } else { - ext = '' + ext = "" } - let fileName = `${path.basename(filePath)}` - if (!fileName.includes('.')) { - fileName += ext + let fileName = `${path.basename(filePath)}`; + if (fileName.indexOf(".") === -1) { + fileName += ext; } const mediaPath = await callNTQQApi({ methodName: NTQQApiMethod.MEDIA_FILE_PATH, args: [{ path_info: { md5HexStr: md5, - fileName, - elementType, + fileName: fileName, + elementType: elementType, elementSubType: 0, thumbSize: 0, needCreate: true, downloadType: 1, - file_uuid: '' + file_uuid: "" } }] }) - log('media path', mediaPath) - await NTQQApi.copyFile(filePath, mediaPath) - const fileSize = await NTQQApi.getFileSize(filePath) + log("media path", mediaPath) + await NTQQApi.copyFile(filePath, mediaPath); + const fileSize = await NTQQApi.getFileSize(filePath); return { md5, fileName, @@ -388,16 +407,16 @@ export class NTQQApi { const apiParams = [ { getReq: { - msgId, - chatType, - peerUid, - elementId, + msgId: msgId, + chatType: chatType, + peerUid: peerUid, + elementId: elementId, thumbSize: 0, downloadType: 1, - filePath: thumbPath - } + filePath: thumbPath, + }, }, - undefined + undefined, ] // log("需要下载media", sourcePath); await callNTQQApi({ @@ -406,7 +425,7 @@ export class NTQQApi { cbCmd: ReceiveCmd.MEDIA_DOWNLOAD_COMPLETE, cmdCB: (payload: { notifyInfo: { filePath: string } }) => { // log("media 下载完成判断", payload.notifyInfo.filePath, sourcePath); - return payload.notifyInfo.filePath == sourcePath + return payload.notifyInfo.filePath == sourcePath; } }) return sourcePath @@ -426,30 +445,30 @@ export class NTQQApi { const peerUid = peer.peerUid // 等待上一个相同的peer发送完 - let checkLastSendUsingTime = 0 + let checkLastSendUsingTime = 0; const waitLastSend = async () => { if (checkLastSendUsingTime > timeout) { - throw ('发送超时') + throw ("发送超时") } - const lastSending = sendMessagePool[peer.peerUid] + let lastSending = sendMessagePool[peer.peerUid] if (lastSending) { // log("有正在发送的消息,等待中...") - await sleep(500) - checkLastSendUsingTime += 500 - return await waitLastSend() + await sleep(500); + checkLastSendUsingTime += 500; + return await waitLastSend(); } else { - + return; } } - await waitLastSend() + await waitLastSend(); - let sentMessage: RawMessage = null + let sentMessage: RawMessage = null; sendMessagePool[peerUid] = async (rawMessage: RawMessage) => { - delete sendMessagePool[peerUid] - sentMessage = rawMessage + delete sendMessagePool[peerUid]; + sentMessage = rawMessage; } - let checkSendCompleteUsingTime = 0 + let checkSendCompleteUsingTime = 0; const checkSendComplete = async (): Promise => { if (sentMessage) { if (waitComplete) { @@ -469,14 +488,13 @@ export class NTQQApi { await sleep(500) return await checkSendComplete() } - log("开始发送消息", peer, msgElements) + callNTQQApi({ methodName: NTQQApiMethod.SEND_MSG, args: [{ - msgId: '0', - peer, - msgElements, - msgAttributeInfos: new Map() + msgId: "0", + peer, msgElements, + msgAttributeInfos: new Map(), }, null] }).then() return await checkSendComplete() @@ -512,13 +530,13 @@ export class NTQQApi { commentElements: [], msgAttributeInfos: new Map() }, - null + null, ] return await new Promise((resolve, reject) => { let complete = false setTimeout(() => { if (!complete) { - reject('转发消息超时') + reject("转发消息超时"); } }, 5000) registerReceiveHook(ReceiveCmd.SELF_SEND_MSG, async (payload: { msgRecord: RawMessage }) => { @@ -544,10 +562,10 @@ export class NTQQApi { methodName: NTQQApiMethod.MULTI_FORWARD_MSG, args: apiArgs }).then(result => { - log('转发消息结果:', result, apiArgs) + log("转发消息结果:", result, apiArgs) if (result.result !== 0) { - complete = true - reject('转发消息失败,' + JSON.stringify(result)) + complete = true; + reject("转发消息失败," + JSON.stringify(result)); } }) }) @@ -558,56 +576,56 @@ export class NTQQApi { // 加群通知,退出通知,需要管理员权限 callNTQQApi({ methodName: ReceiveCmd.GROUP_NOTIFY, - classNameIsRegister: true + classNameIsRegister: true, }).then() return await callNTQQApi({ methodName: NTQQApiMethod.GET_GROUP_NOTICE, cbCmd: ReceiveCmd.GROUP_NOTIFY, afterFirstCmd: false, args: [ - {doubt: false, startSeq: '', number: 14}, + {"doubt": false, "startSeq": "", "number": 14}, null ] - }) + }); } static async handleGroupRequest(seq: string, operateType: GroupRequestOperateTypes, reason?: string) { - const notify: GroupNotify = groupNotifies[seq] + const notify: GroupNotify = groupNotifies[seq]; if (!notify) { throw `${seq}对应的加群通知不存在` } - delete groupNotifies[seq] + delete groupNotifies[seq]; return await callNTQQApi({ methodName: NTQQApiMethod.HANDLE_GROUP_REQUEST, args: [ { - doubt: false, - operateMsg: { - operateType, // 2 拒绝 - targetMsg: { - seq, // 通知序列号 - type: notify.type, - groupCode: notify.group.groupCode, - postscript: reason + "doubt": false, + "operateMsg": { + "operateType": operateType, // 2 拒绝 + "targetMsg": { + "seq": seq, // 通知序列号 + "type": notify.type, + "groupCode": notify.group.groupCode, + "postscript": reason } } }, null ] - }) + }); } static async quitGroup(groupQQ: string) { await callNTQQApi({ methodName: NTQQApiMethod.QUIT_GROUP, args: [ - {groupCode: groupQQ}, + {"groupCode": groupQQ}, null ] }) } - static async handleFriendRequest(sourceId: number, accept: boolean) { + static async handleFriendRequest(sourceId: number, accept: boolean,) { const request: FriendRequest = friendRequests[sourceId] if (!request) { throw `sourceId ${sourceId}, 对应的好友请求不存在` @@ -616,16 +634,16 @@ export class NTQQApi { methodName: NTQQApiMethod.HANDLE_FRIEND_REQUEST, args: [ { - approvalInfo: { - friendUid: request.friendUid, - reqTime: request.reqTime, + "approvalInfo": { + "friendUid": request.friendUid, + "reqTime": request.reqTime, accept } } ] }) - delete friendRequests[sourceId] - return result + delete friendRequests[sourceId]; + return result; } static async kickMember(groupQQ: string, kickUids: string[], refuseForever: boolean = false, kickReason: string = '') { @@ -637,7 +655,7 @@ export class NTQQApi { groupCode: groupQQ, kickUids, refuseForever, - kickReason + kickReason, } ] } @@ -652,7 +670,7 @@ export class NTQQApi { args: [ { groupCode: groupQQ, - memList + memList, } ] } @@ -712,4 +730,107 @@ export class NTQQApi { static publishGroupBulletin(groupQQ: string, title: string, content: string) { } -} + + static async setCacheSilentScan(isSilent: boolean = true) { + return await callNTQQApi({ + methodName: NTQQApiMethod.CACHE_SET_SILENCE, + args: [{ + isSilent + }, null] + }); + } + + static addCacheScannedPaths(pathMap: object = {}) { + return callNTQQApi({ + methodName: NTQQApiMethod.CACHE_ADD_SCANNED_PATH, + args: [{ + pathMap: {...pathMap}, + }, null] + }); + } + + static scanCache() { + callNTQQApi({ + methodName: ReceiveCmd.CACHE_SCAN_FINISH, + classNameIsRegister: true, + }).then(); + return callNTQQApi({ + methodName: NTQQApiMethod.CACHE_SCAN, + args: [null, null], + timeoutSecond: 300, + }); + } + + static getHotUpdateCachePath() { + return callNTQQApi({ + className: NTQQApiClass.HOTUPDATE_API, + methodName: NTQQApiMethod.CACHE_PATH_HOT_UPDATE + }); + } + + static getDesktopTmpPath() { + return callNTQQApi({ + className: NTQQApiClass.BUSINESS_API, + methodName: NTQQApiMethod.CACHE_PATH_DESKTOP_TEMP + }); + } + + static getCacheSessionPathList() { + return callNTQQApi<{ + key: string, + value: string + }[]>({ + className: NTQQApiClass.OS_API, + methodName: NTQQApiMethod.CACHE_PATH_SESSION, + }); + } + + static clearCache(cacheKeys: Array = [ 'tmp', 'hotUpdate' ]) { + return callNTQQApi({ // TODO: 目前还不知道真正的返回值是什么 + methodName: NTQQApiMethod.CACHE_CLEAR, + args: [{ + keys: cacheKeys + }, null] + }); + } + + static getChatCacheList(type: ChatType, pageSize: number = 1000, pageIndex: number = 0) { + return new Promise((res, rej) => { + callNTQQApi({ + methodName: NTQQApiMethod.CACHE_CHAT_GET, + args: [{ + chatType: type, + pageSize, + order: 1, + pageIndex + }, null] + }).then(list => res(list)) + .catch(e => rej(e)); + }); + } + + static getFileCacheInfo(fileType: CacheFileType, pageSize: number = 1000, lastRecord?: CacheFileListItem) { + const _lastRecord = lastRecord ? lastRecord : { fileType: fileType }; + + return callNTQQApi({ + methodName: NTQQApiMethod.CACHE_FILE_GET, + args: [{ + fileType: fileType, + restart: true, + pageSize: pageSize, + order: 1, + lastRecord: _lastRecord, + }, null] + }) + } + + static async clearChatCache(chats: ChatCacheListItemBasic[] = [], fileKeys: string[] = []) { + return await callNTQQApi({ + methodName: NTQQApiMethod.CACHE_CHAT_CLEAR, + args: [{ + chats, + fileKeys + }, null] + }); + } +} \ No newline at end of file diff --git a/src/ntqqapi/types.ts b/src/ntqqapi/types.ts index e412284..62e9e9e 100644 --- a/src/ntqqapi/types.ts +++ b/src/ntqqapi/types.ts @@ -403,4 +403,68 @@ export interface FriendRequestNotify { unreadNums: number, buddyReqs: FriendRequest[] } -} \ No newline at end of file +} + +export interface CacheScanResult { + result: number, + size: [ // 单位为字节 + string, // 系统总存储空间 + string, // 系统可用存储空间 + string, // 系统已用存储空间 + string, // QQ总大小 + string, // 「聊天与文件」大小 + string, // 未知 + string, // 「缓存数据」大小 + string, // 「其他数据」大小 + string, // 未知 + ] +} + +export interface ChatCacheList { + pageCount: number, + infos: ChatCacheListItem[] +} + +export interface ChatCacheListItem { + chatType: ChatType, + basicChatCacheInfo: ChatCacheListItemBasic, + guildChatCacheInfo: unknown[] // TODO: 没用过频道所以不知道这里边的详细内容 +} + +export interface ChatCacheListItemBasic { + chatSize: string, + chatTime: string, + uid: string, + uin: string, + remarkName: string, + nickName: string, + chatType?: ChatType, + isChecked?: boolean +} + +export enum CacheFileType { + IMAGE = 0, + VIDEO = 1, + AUDIO = 2, + DOCUMENT = 3, + OTHER = 4, +} + +export interface CacheFileList { + infos: CacheFileListItem[], +} + +export interface CacheFileListItem { + fileSize: string, + fileTime: string, + fileKey: string, + elementId: string, + elementIdStr: string, + fileType: CacheFileType, + path: string, + fileName: string, + senderId: string, + previewPath: string, + senderName: string, + isChecked?: boolean, +} diff --git a/src/onebot11/action/CleanCache.ts b/src/onebot11/action/CleanCache.ts new file mode 100644 index 0000000..84dd2e6 --- /dev/null +++ b/src/onebot11/action/CleanCache.ts @@ -0,0 +1,103 @@ +import BaseAction from "./BaseAction"; +import {ActionName} from "./types"; +import {NTQQApi} from "../../ntqqapi/ntcall"; +import fs from "fs"; +import Path from "path"; +import { + ChatType, + ChatCacheListItemBasic, + CacheFileType +} from '../../ntqqapi/types'; + +export default class CleanCache extends BaseAction { + actionName = ActionName.CleanCache + + protected _handle(): Promise { + return new Promise(async (res, rej) => { + try { + const cacheFilePaths: string[] = []; + + await NTQQApi.setCacheSilentScan(false); + + cacheFilePaths.push((await NTQQApi.getHotUpdateCachePath())); + cacheFilePaths.push((await NTQQApi.getDesktopTmpPath())); + (await NTQQApi.getCacheSessionPathList()).forEach(e => cacheFilePaths.push(e.value)); + + // await NTQQApi.addCacheScannedPaths(); // XXX: 调用就崩溃,原因目前还未知 + const cacheScanResult = await NTQQApi.scanCache(); + const cacheSize = parseInt(cacheScanResult.size[6]); + + if (cacheScanResult.result !== 0) { + throw('Something went wrong while scanning cache. Code: ' + cacheScanResult.result); + } + + await NTQQApi.setCacheSilentScan(true); + if (cacheSize > 0 && cacheFilePaths.length > 2) { // 存在缓存文件且大小不为 0 时执行清理动作 + // await NTQQApi.clearCache([ 'tmp', 'hotUpdate', ...cacheScanResult ]) // XXX: 也是调用就崩溃,调用 fs 删除得了 + deleteCachePath(cacheFilePaths); + } + + // 获取聊天记录列表 + // NOTE: 以防有人不需要删除聊天记录,暂时先注释掉,日后加个开关 + // const privateChatCache = await getCacheList(ChatType.friend); // 私聊消息 + // const groupChatCache = await getCacheList(ChatType.group); // 群聊消息 + // const chatCacheList = [ ...privateChatCache, ...groupChatCache ]; + const chatCacheList: ChatCacheListItemBasic[] = []; + + // 获取聊天缓存文件列表 + const cacheFileList: string[] = []; + + for (const name in CacheFileType) { + if (!isNaN(parseInt(name))) continue; + + const fileTypeAny: any = CacheFileType[name]; + const fileType: CacheFileType = fileTypeAny; + + cacheFileList.push(...(await NTQQApi.getFileCacheInfo(fileType)).infos.map(file => file.fileKey)); + } + + // 一并清除 + await NTQQApi.clearChatCache(chatCacheList, cacheFileList); + res(); + } catch(e) { + console.error('清理缓存时发生了错误'); + rej(e); + } + }); + } +} + +function deleteCachePath(pathList: string[]) { + const emptyPath = (path: string) => { + if (!fs.existsSync(path)) return; + const files = fs.readdirSync(path); + files.forEach(file => { + const filePath = Path.resolve(path, file); + const stats = fs.statSync(filePath); + if (stats.isDirectory()) emptyPath(filePath); + else fs.unlinkSync(filePath); + }); + fs.rmdirSync(path); + } + + for (const path of pathList) { + emptyPath(path); + } +} + +function getCacheList(type: ChatType) { // NOTE: 做这个方法主要是因为目前还不支持针对频道消息的清理 + return new Promise>((res, rej) => { + NTQQApi.getChatCacheList(type, 1000, 0) + .then(data => { + const list = data.infos.filter(e => e.chatType === type && parseInt(e.basicChatCacheInfo.chatSize) > 0); + const result = list.map(e => { + const result = { ...e.basicChatCacheInfo }; + result.chatType = type; + result.isChecked = true; + return result; + }); + res(result); + }) + .catch(e => rej(e)); + }); +} \ No newline at end of file diff --git a/src/onebot11/action/index.ts b/src/onebot11/action/index.ts index 5abe960..ac63ef1 100644 --- a/src/onebot11/action/index.ts +++ b/src/onebot11/action/index.ts @@ -31,6 +31,7 @@ import SetGroupCard from "./SetGroupCard"; import GetImage from "./GetImage"; import GetRecord from "./GetRecord"; import GoCQHTTPMarkMsgAsRead from "./MarkMsgAsRead"; +import CleanCache from "./CleanCache"; export const actionHandlers = [ new Debug(), @@ -56,6 +57,7 @@ export const actionHandlers = [ new SetGroupCard(), new GetImage(), new GetRecord(), + new CleanCache(), //以下为go-cqhttp api new GoCQHTTPSendGroupForwardMsg(), diff --git a/src/onebot11/action/types.ts b/src/onebot11/action/types.ts index c7843c0..9ab5f2c 100644 --- a/src/onebot11/action/types.ts +++ b/src/onebot11/action/types.ts @@ -42,6 +42,7 @@ export enum ActionName { SetGroupName = "set_group_name", GetImage = "get_image", GetRecord = "get_record", + CleanCache = "clean_cache", // 以下为go-cqhttp api GoCQHTTP_SendGroupForwardMsg = "send_group_forward_msg", GoCQHTTP_SendPrivateForwardMsg = "send_private_forward_msg",