feat: clean cache api

This commit is contained in:
linyuchen 2024-03-09 22:58:21 +08:00
commit 354ee389bc
6 changed files with 567 additions and 274 deletions

@ -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<string, (apiReturn: any) => void> = {}
export let hookApiCallbacks: Record<string, (apiReturn: any) => 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<PayloadType = unknown> extends Array<any> {
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<void>)
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<void>).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<PayloadType>(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<string>() // 建立索引降低时间复杂度
group.members = newMembers;
const newMembersSet = new Set<string>(); // 建立索引降低时间复杂度
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<string>()
const oldMembers = existGroup.members;
const oldMembersSet = new Set<string>();
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<RawMessage> }>(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)
}
}
})

@ -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<ReturnType>(params: NTQQApiParams) {
function callNTQQApi<ReturnType>(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<ReturnType>(params: NTQQApiParams) {
hookApiCallbacks[uuid] = (r: ReturnType) => {
success = true
resolve(r)
}
};
} else {
// 这里的callback比较特殊QQ后端先返回是否调用成功再返回一条结果数据
const secondCallback = () => {
const hookId = registerReceiveHook<ReturnType>(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<ReturnType>(params: NTQQApiParams) {
})
}
export const sendMessagePool: Record<string, ((sendSuccessMsg: RawMessage) => void) | null> = {}// peerUid: callbackFunnc
export let sendMessagePool: Record<string, ((sendSuccessMsg: RawMessage) => void) | null> = {}// peerUid: callbackFunnc
interface GeneralCallResult {
result: number // 0: success
result: number, // 0: success
errMsg: string
}
export class NTQQApi {
// static likeFriend = defineNTQQApi<void>(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<SelfInfo>({
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<string>({
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<RawMessage> => {
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<RawMessage>((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<GeneralCallResult>({
methodName: ReceiveCmd.GROUP_NOTIFY,
classNameIsRegister: true
classNameIsRegister: true,
}).then()
return await callNTQQApi<GroupNotifies>({
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<GeneralCallResult>({
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<GeneralCallResult>({
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<GeneralCallResult>({
methodName: NTQQApiMethod.CACHE_SET_SILENCE,
args: [{
isSilent
}, null]
});
}
static addCacheScannedPaths(pathMap: object = {}) {
return callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.CACHE_ADD_SCANNED_PATH,
args: [{
pathMap: {...pathMap},
}, null]
});
}
static scanCache() {
callNTQQApi<GeneralCallResult>({
methodName: ReceiveCmd.CACHE_SCAN_FINISH,
classNameIsRegister: true,
}).then();
return callNTQQApi<CacheScanResult>({
methodName: NTQQApiMethod.CACHE_SCAN,
args: [null, null],
timeoutSecond: 300,
});
}
static getHotUpdateCachePath() {
return callNTQQApi<string>({
className: NTQQApiClass.HOTUPDATE_API,
methodName: NTQQApiMethod.CACHE_PATH_HOT_UPDATE
});
}
static getDesktopTmpPath() {
return callNTQQApi<string>({
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<string> = [ 'tmp', 'hotUpdate' ]) {
return callNTQQApi<any>({ // TODO: 目前还不知道真正的返回值是什么
methodName: NTQQApiMethod.CACHE_CLEAR,
args: [{
keys: cacheKeys
}, null]
});
}
static getChatCacheList(type: ChatType, pageSize: number = 1000, pageIndex: number = 0) {
return new Promise<ChatCacheList>((res, rej) => {
callNTQQApi<ChatCacheList>({
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<CacheFileList>({
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<GeneralCallResult>({
methodName: NTQQApiMethod.CACHE_CHAT_CLEAR,
args: [{
chats,
fileKeys
}, null]
});
}
}

@ -403,4 +403,68 @@ export interface FriendRequestNotify {
unreadNums: number,
buddyReqs: FriendRequest[]
}
}
}
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,
}

@ -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<void, void> {
actionName = ActionName.CleanCache
protected _handle(): Promise<void> {
return new Promise<void>(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<Array<ChatCacheListItemBasic>>((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));
});
}

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

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