LLOneBot/src/ntqqapi/hook.ts
2024-05-18 13:09:45 +08:00

531 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<string, (apiReturn: any) => 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<PayloadType = unknown> extends Array<any> {
0: {
type: 'request'
eventName: NTQQApiClass
callbackId?: string
}
1: {
cmdName: ReceiveCmd
cmdType: 'event'
payload: PayloadType
}[]
}
let receiveHooks: Array<{
method: ReceiveCmd[]
hookFunc: (payload: any) => void | Promise<void>
id: string
}> = []
let callHooks: Array<{
method: NTQQApiMethod[]
hookFunc: (callParams: unknown[]) => void | Promise<void>
}> = []
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<void>).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<void>).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<PayloadType>(
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>,
): 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<string>() // 建立索引降低时间复杂度
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<GroupMember>
}>(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<RawMessage> }>([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 })
})
})