import { BrowserWindow } from 'electron' import { NTQQApiClass, NTQQApiMethod } from './ntcall' import { NTQQMsgApi, sendMessagePool } from './api/msg' import { CategoryFriend, ChatType, Group, GroupMember, GroupMemberRole, RawMessage, User } from './types' import { deleteGroup, friends, getFriend, getGroupMember, groups, rawFriends, selfInfo, tempGroupCodeMap, uidMaps, } from '@/common/data' import { OB11GroupDecreaseEvent } from '../onebot11/event/notice/OB11GroupDecreaseEvent' import { v4 as uuidv4 } from 'uuid' import { postOb11Event } from '../onebot11/server/post-ob11-event' import { getConfigUtil, HOOK_LOG } from '@/common/config' import fs from 'fs' import { dbUtil } from '@/common/db' import { NTQQGroupApi } from './api/group' import { log } from '@/common/utils' import { isNumeric, sleep } from '@/common/utils' import { OB11Constructor } from '../onebot11/constructor' export let hookApiCallbacks: Record void> = {} export let ReceiveCmdS = { RECENT_CONTACT: 'nodeIKernelRecentContactListener/onRecentContactListChangedVer2', UPDATE_MSG: 'nodeIKernelMsgListener/onMsgInfoListUpdate', UPDATE_ACTIVE_MSG: 'nodeIKernelMsgListener/onActiveMsgInfoUpdate', NEW_MSG: `nodeIKernelMsgListener/onRecvMsg`, NEW_ACTIVE_MSG: `nodeIKernelMsgListener/onRecvActiveMsg`, SELF_SEND_MSG: 'nodeIKernelMsgListener/onAddSendMsg', USER_INFO: 'nodeIKernelProfileListener/onProfileSimpleChanged', USER_DETAIL_INFO: 'nodeIKernelProfileListener/onProfileDetailInfoChanged', GROUPS: 'nodeIKernelGroupListener/onGroupListUpdate', GROUPS_STORE: 'onGroupListUpdate', GROUP_MEMBER_INFO_UPDATE: 'nodeIKernelGroupListener/onMemberInfoChange', 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', MEDIA_UPLOAD_COMPLETE: 'nodeIKernelMsgListener/onRichMediaUploadComplete', SKEY_UPDATE: 'onSkeyUpdate', } export type ReceiveCmd = (typeof ReceiveCmdS)[keyof typeof ReceiveCmdS] interface NTQQApiReturnData extends Array { 0: { type: 'request' eventName: NTQQApiClass callbackId?: string } 1: { cmdName: ReceiveCmd cmdType: 'event' payload: PayloadType }[] } let receiveHooks: Array<{ method: ReceiveCmd[] hookFunc: (payload: any) => void | Promise id: string }> = [] let callHooks: Array<{ method: NTQQApiMethod[] hookFunc: (callParams: unknown[]) => void | Promise }> = [] export function hookNTQQApiReceive(window: BrowserWindow) { const originalSend = window.webContents.send const patchSend = (channel: string, ...args: NTQQApiReturnData) => { // console.log("hookNTQQApiReceive", channel, args) let isLogger = false try { isLogger = args[0]?.eventName?.startsWith('ns-LoggerApi') } catch (e) {} if (!isLogger) { try { HOOK_LOG && log(`received ntqq api message: ${channel}`, args) } catch (e) { log('hook log error', e, args) } } try { if (args?.[1] instanceof Array) { for (let receiveData of args?.[1]) { const ntQQApiMethodName = receiveData.cmdName // log(`received ntqq api message: ${channel} ${ntQQApiMethodName}`, JSON.stringify(receiveData)) for (let hook of receiveHooks) { if (hook.method.includes(ntQQApiMethodName)) { new Promise((resolve, reject) => { try { let _ = hook.hookFunc(receiveData.payload) if (hook.hookFunc.constructor.name === 'AsyncFunction') { ;(_ as Promise).then() } } catch (e) { log('hook error', e, 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] } } } catch (e) { log('hookNTQQApiReceive error', e.stack.toString(), args) } originalSend.call(window.webContents, channel, ...args) } window.webContents.send = patchSend } export function hookNTQQApiCall(window: BrowserWindow) { // 监听调用NTQQApi 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) { // console.log(thisArg, args); let isLogger = false try { isLogger = args[3][0].eventName.startsWith('ns-LoggerApi') } catch (e) {} if (!isLogger) { try { HOOK_LOG && log('call NTQQ api', thisArg, args) } catch (e) {} try { const _args: unknown[] = args[3][1] const cmdName: NTQQApiMethod = _args[0] as NTQQApiMethod const callParams = _args.slice(1) callHooks.forEach((hook) => { if (hook.method.includes(cmdName)) { new Promise((resolve, reject) => { try { let _ = hook.hookFunc(callParams) if (hook.hookFunc.constructor.name === 'AsyncFunction') { ;(_ as Promise).then() } } catch (e) { log('hook call error', e, _args) } }).then() } }) } catch (e) {} } return target.apply(thisArg, args) }, }) if (webContents._events['-ipc-message']?.[0]) { webContents._events['-ipc-message'][0] = proxyIpcMsg } else { webContents._events['-ipc-message'] = proxyIpcMsg } const ipc_invoke_proxy = webContents._events['-ipc-invoke']?.[0] || webContents._events['-ipc-invoke'] const proxyIpcInvoke = new Proxy(ipc_invoke_proxy, { apply(target, thisArg, args) { // console.log(args); HOOK_LOG && log('call NTQQ invoke api', thisArg, args) args[0]['_replyChannel']['sendReply'] = new Proxy(args[0]['_replyChannel']['sendReply'], { apply(sendtarget, sendthisArg, sendargs) { sendtarget.apply(sendthisArg, sendargs) }, }) let ret = target.apply(thisArg, args) try { HOOK_LOG && log('call NTQQ invoke api return', ret) } catch (e) {} return ret }, }) if (webContents._events['-ipc-invoke']?.[0]) { webContents._events['-ipc-invoke'][0] = proxyIpcInvoke } else { webContents._events['-ipc-invoke'] = proxyIpcInvoke } } export function registerReceiveHook( method: ReceiveCmd | ReceiveCmd[], hookFunc: (payload: PayloadType) => void, ): string { const id = uuidv4() if (!Array.isArray(method)) { method = [method] } receiveHooks.push({ method, hookFunc, id, }) return id } export function registerCallHook( method: NTQQApiMethod | NTQQApiMethod[], hookFunc: (callParams: unknown[]) => void | Promise, ): void { if (!Array.isArray(method)) { method = [method] } callHooks.push({ method, hookFunc, }) } export function removeReceiveHook(id: string) { const index = receiveHooks.findIndex((h) => h.id === id) receiveHooks.splice(index, 1) } let activatedGroups: string[] = [] async function updateGroups(_groups: Group[], needUpdate: boolean = true) { for (let group of _groups) { log('update group', group) if (group.privilegeFlag === 0) { deleteGroup(group.groupCode) continue } log('update group', group) // if (!activatedGroups.includes(group.groupCode)) { NTQQMsgApi.activateChat({ peerUid: group.groupCode, chatType: ChatType.group }) .then((r) => { // activatedGroups.push(group.groupCode); // log(`激活群聊天窗口${group.groupName}(${group.groupCode})`, r) // if (r.result !== 0) { // setTimeout(() => NTQQMsgApi.activateGroupChat(group.groupCode).then(r => log(`再次激活群聊天窗口${group.groupName}(${group.groupCode})`, r)), 500); // }else { // } }) .catch(log) // } let existGroup = groups.find((g) => g.groupCode == group.groupCode) if (existGroup) { Object.assign(existGroup, group) } else { groups.push(group) existGroup = group } if (needUpdate) { const members = await NTQQGroupApi.getGroupMembers(group.groupCode) if (members) { existGroup.members = members } } } } async function processGroupEvent(payload: { groupList: Group[] }) { try { const newGroupList = payload.groupList for (const group of newGroupList) { let existGroup = groups.find((g) => g.groupCode == group.groupCode) if (existGroup) { if (existGroup.memberCount > group.memberCount) { log(`群(${group.groupCode})成员数量减少${existGroup.memberCount} -> ${group.memberCount}`) const oldMembers = existGroup.members await sleep(200) // 如果请求QQ API的速度过快,通常无法正确拉取到最新的群信息,因此这里人为引入一个延时 const newMembers = await NTQQGroupApi.getGroupMembers(group.groupCode) group.members = newMembers const newMembersSet = new Set() // 建立索引降低时间复杂度 for (const member of newMembers) { newMembersSet.add(member.uin) } // 判断bot是否是管理员,如果是管理员不需要从这里得知有人退群,这里的退群无法得知是主动退群还是被踢 let bot = await getGroupMember(group.groupCode, selfInfo.uin) if (bot.role == GroupMemberRole.admin || bot.role == GroupMemberRole.owner) { continue } for (const member of oldMembers) { if (!newMembersSet.has(member.uin) && member.uin != selfInfo.uin) { postOb11Event( new OB11GroupDecreaseEvent( parseInt(group.groupCode), parseInt(member.uin), parseInt(member.uin), 'leave', ), ) break } } } if (group.privilegeFlag === 0) { deleteGroup(group.groupCode) } } } updateGroups(newGroupList, false).then() } catch (e) { updateGroups(payload.groupList).then() log('更新群信息错误', e.stack.toString()) } } // 群列表变动 registerReceiveHook<{ groupList: Group[]; updateType: number }>(ReceiveCmdS.GROUPS, (payload) => { // updateType 3是群列表变动,2是群成员变动 // log("群列表变动", payload.updateType, payload.groupList) if (payload.updateType != 2) { updateGroups(payload.groupList).then() } else { if (process.platform == 'win32') { processGroupEvent(payload).then() } } }) registerReceiveHook<{ groupList: Group[]; updateType: number }>(ReceiveCmdS.GROUPS_STORE, (payload) => { // updateType 3是群列表变动,2是群成员变动 // log("群列表变动", payload.updateType, payload.groupList) if (payload.updateType != 2) { updateGroups(payload.groupList).then() } else { if (process.platform != 'win32') { processGroupEvent(payload).then() } } }) registerReceiveHook<{ groupCode: string dataSource: number members: Set }>(ReceiveCmdS.GROUP_MEMBER_INFO_UPDATE, async (payload) => { const groupCode = payload.groupCode const members = Array.from(payload.members.values()) // log("群成员信息变动", groupCode, members) for (const member of members) { const existMember = await getGroupMember(groupCode, member.uin) if (existMember) { Object.assign(existMember, member) } } // const existGroup = groups.find(g => g.groupCode == groupCode); // if (existGroup) { // log("对比群成员", existGroup.members, members) // for (const member of members) { // const existMember = existGroup.members.find(m => m.uin == member.uin); // if (existMember) { // log("对比群名片", existMember.cardName, member.cardName) // if (existMember.cardName != member.cardName) { // postOB11Event(new OB11GroupCardEvent(parseInt(existGroup.groupCode), parseInt(member.uin), member.cardName, existMember.cardName)); // } // Object.assign(existMember, member); // } // } // } }) // 好友列表变动 registerReceiveHook<{ data:CategoryFriend[] }>(ReceiveCmdS.FRIENDS, (payload) => { rawFriends.length = 0; rawFriends.push(...payload.data); for (const fData of payload.data) { const _friends = fData.buddyList for (let friend of _friends) { NTQQMsgApi.activateChat({ peerUid: friend.uid, chatType: ChatType.friend }).then() let existFriend = friends.find((f) => f.uin == friend.uin) if (!existFriend) { friends.push(friend) } else { Object.assign(existFriend, friend) } } } }) registerReceiveHook<{ msgList: Array }>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], (payload) => { // 保存一下uid for (const message of payload.msgList) { const uid = message.senderUid const uin = message.senderUin if (uid && uin) { if (message.chatType === ChatType.temp) { dbUtil.getReceivedTempUinMap().then((receivedTempUinMap) => { if (!receivedTempUinMap[uin]) { receivedTempUinMap[uin] = uid dbUtil.setReceivedTempUinMap(receivedTempUinMap) } }) } uidMaps[uid] = uin } } // 自动清理新消息文件 const { autoDeleteFile } = getConfigUtil().getConfig() if (!autoDeleteFile) { return } for (const message of payload.msgList) { // log("收到新消息,push到历史记录", message.msgId) // dbUtil.addMsg(message).then() // 清理文件 for (const msgElement of message.elements) { setTimeout(() => { const picPath = msgElement.picElement?.sourcePath const picThumbPath = [...msgElement.picElement?.thumbPath.values()] const pttPath = msgElement.pttElement?.filePath const filePath = msgElement.fileElement?.filePath const videoPath = msgElement.videoElement?.filePath const videoThumbPath: string[] = [...msgElement.videoElement?.thumbPath.values()] const pathList = [picPath, ...picThumbPath, pttPath, filePath, videoPath, ...videoThumbPath] if (msgElement.picElement) { pathList.push(...Object.values(msgElement.picElement.thumbPath)) } const aioOpGrayTipElement = msgElement.grayTipElement?.aioOpGrayTipElement if (aioOpGrayTipElement) { tempGroupCodeMap[aioOpGrayTipElement.peerUid] = aioOpGrayTipElement.fromGrpCodeOfTmpChat } // log("需要清理的文件", pathList); for (const path of pathList) { if (path) { fs.unlink(picPath, () => { log('删除文件成功', path) }) } } }, getConfigUtil().getConfig().autoDeleteFileSecond * 1000) } } }) registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, ({ msgRecord }) => { const message = msgRecord const peerUid = message.peerUid // log("收到自己发送成功的消息", Object.keys(sendMessagePool), message); // log("收到自己发送成功的消息", message.msgId, message.msgSeq); dbUtil.addMsg(message).then() const sendCallback = sendMessagePool[peerUid] if (sendCallback) { try { sendCallback(message) } catch (e) { log('receive self msg error', e.stack) } } }) registerReceiveHook<{ info: { status: number } }>(ReceiveCmdS.SELF_STATUS, (info) => { selfInfo.online = info.info.status !== 20 }) let activatedPeerUids: string[] = [] registerReceiveHook<{ changedRecentContactLists: { listType: number sortedContactList: string[] changedList: { id: string // peerUid chatType: ChatType }[] }[] }>(ReceiveCmdS.RECENT_CONTACT, async (payload) => { for (const recentContact of payload.changedRecentContactLists) { for (const changedContact of recentContact.changedList) { if (activatedPeerUids.includes(changedContact.id)) continue activatedPeerUids.push(changedContact.id) const peer = { peerUid: changedContact.id, chatType: changedContact.chatType } if (changedContact.chatType === ChatType.temp) { log('收到临时会话消息', peer) NTQQMsgApi.activateChatAndGetHistory(peer).then(() => { NTQQMsgApi.getMsgHistory(peer, '', 20).then(({ msgList }) => { let lastTempMsg = msgList.pop() log('激活窗口之前的第一条临时会话消息:', lastTempMsg) if (Date.now() / 1000 - parseInt(lastTempMsg.msgTime) < 5) { OB11Constructor.message(lastTempMsg).then((r) => postOb11Event(r)) } }) }) } else { NTQQMsgApi.activateChat(peer).then() } } } }) registerCallHook(NTQQApiMethod.DELETE_ACTIVE_CHAT, async (payload) => { const peerUid = payload[0] as string log('激活的聊天窗口被删除,准备重新激活', peerUid) let chatType = ChatType.friend if (isNumeric(peerUid)) { chatType = ChatType.group } else { // 检查是否好友 if (!(await getFriend(peerUid))) { chatType = ChatType.temp } } const peer = { peerUid, chatType } await sleep(1000) NTQQMsgApi.activateChat(peer).then((r) => { log('重新激活聊天窗口', peer, { result: r.result, errMsg: r.errMsg }) }) })