From d02a0fceb2684112ef79d410b5c3f10a85669256 Mon Sep 17 00:00:00 2001 From: "Wesley F. Young" Date: Wed, 28 Aug 2024 22:21:40 +0800 Subject: [PATCH] feat: parse event from raw message --- src/core/events.ts | 362 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 343 insertions(+), 19 deletions(-) diff --git a/src/core/events.ts b/src/core/events.ts index 89686f44..2fc4d7ec 100644 --- a/src/core/events.ts +++ b/src/core/events.ts @@ -1,15 +1,18 @@ import { BuddyReqType, - ChatType, DataSource, + ChatType, + DataSource, FileElement, FriendRequest, - GrayTipElement, GroupMemberRole, + GrayTipElement, + GroupMemberRole, GroupNotify, GroupNotifyMsgStatus, GroupNotifyMsgType, + NTGrayTipElementSubTypeV2, RawMessage, SendStatusType, - TipGroupElement, + TipGroupElementType, } from '@/core/entities'; import { NodeIKernelBuddyListener, NodeIKernelGroupListener, NodeIKernelMsgListener } from '@/core/listeners'; import EventEmitter from 'node:events'; @@ -17,6 +20,7 @@ import TypedEmitter from 'typed-emitter/rxjs'; import { NapCatCore } from '@/core/index'; import { LRUCache } from '@/common/lru-cache'; import { proxiedListenerOf } from '@/common/proxy-handler'; +import fastXmlParser from 'fast-xml-parser'; type NapCatInternalEvents = { 'message/receive': (msg: RawMessage) => PromiseLike; @@ -28,10 +32,10 @@ type NapCatInternalEvents = { xRequest: FriendRequest) => PromiseLike; 'buddy/add': (uin: string, - xMsg: RawMessage) => PromiseLike; + xGrayTipElement: GrayTipElement, xMsg: RawMessage) => PromiseLike; - 'buddy/poke': (initiatorUin: string, targetUin: string, displayMsg: string, - xMsg: RawMessage) => PromiseLike; + 'buddy/poke': (initiatorUin: string, targetUin: string, + xGrayTipElement: GrayTipElement, xMsg: RawMessage) => PromiseLike; 'buddy/recall': (uin: string, messageId: string, xMsg: RawMessage /* This is not the message that is recalled */) => PromiseLike; @@ -52,20 +56,32 @@ type NapCatInternalEvents = { xDataSource?: RawMessage, xMsg?: RawMessage) => PromiseLike; - 'group/mute': (groupCode: string, targetUin: string, duration: number, operation: 'mute' | 'unmute', + 'group/mute': (groupCode: string, targetUin: string, operatorUin: string, duration: number, xGrayTipElement: GrayTipElement, xMsg: RawMessage) => PromiseLike; + 'group/unmute': (groupCode: string, targetUin: string, operatorUin: string, + xGrayTipElement: GrayTipElement, xMsg: RawMessage) => PromiseLike; + + 'group/mute-all': (groupCode: string, operatorUin: string, + xGrayTipElement: GrayTipElement, xMsg: RawMessage) => PromiseLike; + + 'group/unmute-all': (groupCode: string, operatorUin: string, + xGrayTipElement: GrayTipElement, xMsg: RawMessage) => PromiseLike; + 'group/card-change': (groupCode: string, changedUin: string, newCard: string, oldCard: string, xMsg: RawMessage) => PromiseLike; - 'group/member-increase': (groupCode: string, targetUin: string, operatorUin: string, reason: 'invite' | 'approve' | 'unknown', - xGrayTipElement: GrayTipElement, xMsg: RawMessage) => PromiseLike; + 'group/member-increase/invite': (groupCode: string, newMemberUin: string, invitorUin: string, + xGrayTipElement: GrayTipElement, xMsg: RawMessage) => PromiseLike; - 'group/member-decrease/kicked': (groupCode: string, leftMemberUin: string, operatorUin: string, - xGroupNotify: GroupNotify) => PromiseLike; + 'group/member-increase/active': (groupCode: string, newMemberUin: string, approvalUin: string | undefined, + xGrayTipElement: GrayTipElement, xMsg: RawMessage) => PromiseLike; + + 'group/member-decrease/kick': (groupCode: string, leftMemberUin: string, operatorUin: string, + xGroupNotify: GroupNotify) => PromiseLike; 'group/member-decrease/self-kicked': (groupCode: string, operatorUin: string, - xTipGroupElement: TipGroupElement, xMsg: RawMessage) => PromiseLike; + xGrayTipElement: GrayTipElement, xMsg: RawMessage) => PromiseLike; 'group/member-decrease/leave': (groupCode: string, leftMemberUin: string, xGroupNotify: GroupNotify) => PromiseLike; @@ -76,7 +92,7 @@ type NapCatInternalEvents = { // If it comes from onRecvSysMsg xGrayTipElement?: GrayTipElement, xMsg?: RawMessage) => PromiseLike; - 'group/essence': (groupCode: string, messageId: string, senderUin: string, operation: 'add' | 'delete', + 'group/essence': (groupCode: string, messageId: string, operation: 'add' | 'delete', xGrayTipElement: GrayTipElement, xGrayTipSourceMsg: RawMessage /* this is not the message that is set to be essence msg */) => PromiseLike; @@ -84,7 +100,7 @@ type NapCatInternalEvents = { xGrayTipSourceMsg: RawMessage /* This is not the message that is recalled */) => PromiseLike; 'group/title': (groupCode: string, targetUin: string, newTitle: string, - xMsg: RawMessage) => PromiseLike; + xGrayTipElement: GrayTipElement, xMsg: RawMessage) => PromiseLike; 'group/upload': (groupCode: string, uploaderUin: string, fileElement: FileElement, xMsg: RawMessage) => PromiseLike; @@ -95,7 +111,7 @@ type NapCatInternalEvents = { // If it comes from onRecvSysMsg xSysMsg?: number[]) => PromiseLike; - 'group/poke': (groupCode: string, initiatorUin: string, targetUin: string, displayMsg: string, + 'group/poke': (groupCode: string, initiatorUin: string, targetUin: string, xGrayTipElement: GrayTipElement, xMsg: RawMessage) => PromiseLike; } @@ -115,10 +131,11 @@ export class NapCatEventChannel extends private initMsgListener() { const msgListener = new NodeIKernelMsgListener(); - msgListener.onRecvMsg = msgList => { + msgListener.onRecvMsg = async msgList => { for (const msg of msgList) { if (msg.senderUin !== this.core.selfInfo.uin) { - this.emit('message/receive', msg); + const handled = await this.parseRawMsgToEventAndEmit(msg); + if (!handled) this.emit('message/receive', msg); } } }; @@ -142,12 +159,14 @@ export class NapCatEventChannel extends } this.emit('group/recall', msg.peerUin, operatorId, msg.msgId, msg); } + continue; } // Handle message send if (msg.sendStatus === SendStatusType.KSEND_STATUS_SUCCESS && !msgIdSentCache.get(msg.msgId)) { msgIdSentCache.put(msg.msgId, true); - this.emit('message/send', msg); + const handled = await this.parseRawMsgToEventAndEmit(msg); + if (!handled) this.emit('message/send', msg); } } }; @@ -157,6 +176,290 @@ export class NapCatEventChannel extends ); } + private async parseRawMsgToEventAndEmit(msg: RawMessage) { + let handled = false; + + if (msg.chatType === ChatType.KCHATTYPEC2C) { + for (const element of msg.elements) { + if (element.grayTipElement) { + if (element.grayTipElement.subElementType == NTGrayTipElementSubTypeV2.GRAYTIP_ELEMENT_SUBTYPE_JSON) { + try { + if (element.grayTipElement.jsonGrayTipElement.busiId == 1061) { + const json = JSON.parse(element.grayTipElement.jsonGrayTipElement.jsonStr); + const pokeDetail = (json.items as any[]).filter(item => item.uid); + if (pokeDetail.length == 2) { + this.emit( + 'buddy/poke', + await this.core.apis.UserApi.getUinByUidV2(pokeDetail[0].uid), + await this.core.apis.UserApi.getUinByUidV2(pokeDetail[1].uid)!, + element.grayTipElement, msg, + ); + handled = true; + } + } + } catch (e) { + this.core.context.logger.logError('解析 Poke 消息失败', e); + } + } + + if (element.grayTipElement.subElementType == NTGrayTipElementSubTypeV2.GRAYTIP_ELEMENT_SUBTYPE_XMLMSG) { + try { + if (element.grayTipElement.xmlElement.templId === '10229' && msg.peerUin !== '') { + this.emit( + 'buddy/add', + msg.peerUin, + element.grayTipElement, msg, + ); + handled = true; + } + } catch (e) { + this.core.context.logger.logError('解析好友增加消息失败', e); + } + } + } + } + } else if (msg.chatType === ChatType.KCHATTYPEGROUP) { + for (const element of msg.elements) { + if (element.grayTipElement) { + if (element.grayTipElement.groupElement) { + /* + * Events that are included in groupElements: + * - group/member-increase/active + * - group/mute, ... + * - group/member-decrease/... + */ + + const groupElement = element.grayTipElement.groupElement; + const groupCode = msg.peerUin; + + try { + if (groupElement.type === TipGroupElementType.memberIncrease) { + const member = await this.core.apis.GroupApi.getGroupMember(groupCode, groupElement.memberUid); + const adminMemberOrEmpty = groupElement.adminUid ? + await this.core.apis.GroupApi.getGroupMember(groupCode, groupElement.adminUid) : + undefined; + this.emit( + 'group/member-increase/active', + groupCode, + member!.uin, + adminMemberOrEmpty?.uin, + element.grayTipElement, msg, + ); + handled = true; + } + } catch (e) { + this.core.context.logger.logError('解析群成员增加消息失败', e); + } + + try { + if (groupElement.type === TipGroupElementType.ban) { + const shutUpAttr = groupElement.shutUp!; + const durationOrLiftBan = parseInt(shutUpAttr.duration); + if (shutUpAttr.member?.uid) { + if (durationOrLiftBan > 0) { + this.emit( + 'group/mute', + groupCode, + (await this.core.apis.GroupApi.getGroupMember(groupCode, shutUpAttr.member.uid))!.uin, + (await this.core.apis.GroupApi.getGroupMember(groupCode, shutUpAttr.admin.uid))!.uin, + durationOrLiftBan, + element.grayTipElement, msg, + ); + } else { + this.emit( + 'group/unmute', + groupCode, + (await this.core.apis.GroupApi.getGroupMember(groupCode, shutUpAttr.member.uid))!.uin, + (await this.core.apis.GroupApi.getGroupMember(groupCode, shutUpAttr.admin.uid))!.uin, + element.grayTipElement, msg, + ); + } + } else { + if (durationOrLiftBan > 0) { + this.emit( + 'group/mute-all', + groupCode, + (await this.core.apis.GroupApi.getGroupMember(groupCode, shutUpAttr.admin.uid))!.uin, + element.grayTipElement, msg, + ); + } else { + this.emit( + 'group/unmute-all', + groupCode, + (await this.core.apis.GroupApi.getGroupMember(groupCode, shutUpAttr.admin.uid))!.uin, + element.grayTipElement, msg, + ); + } + } + handled = true; + } + } catch (e) { + this.core.context.logger.logError('解析群禁言消息失败', e); + } + + try { + if (groupElement.type == TipGroupElementType.kicked) { + await this.core.apis.GroupApi.quitGroup(groupCode); + const adminUin = + (await this.core.apis.GroupApi.getGroupMember(groupCode, groupElement.adminUid))?.uin ?? + (await this.core.apis.UserApi.getUinByUidV2(groupElement.adminUid)); + if (adminUin) { + this.emit( + 'group/member-decrease/self-kicked', + groupCode, + adminUin, + element.grayTipElement, msg, + ); + } else { + this.emit( + 'group/member-decrease/unknown', + groupCode, + this.core.selfInfo.uin, + undefined, + element.grayTipElement, msg, + ); + } + handled = true; + } + } catch (e) { + this.core.context.logger.logError('解析退群消息失败', e); + } + } + + if (element.grayTipElement.xmlElement) { + /* + * Events that are included in xmlElement: + * - group/emoji-like + * - group/member-increase/invite + */ + if (element.grayTipElement.xmlElement.templId === '10382') { + const emojiLikeData = new fastXmlParser.XMLParser({ + ignoreAttributes: false, + attributeNamePrefix: '', + }).parse(element.grayTipElement.xmlElement.content); + const groupCode = msg.peerUin; + const senderUin = emojiLikeData.gtip.qq.jp; + const msgSeq = emojiLikeData.gtip.url.msgseq; + const emojiId = emojiLikeData.gtip.face.id; + const likedMsgId = await this.findMsgIdForEmojiLikeEventByMsgSeq(groupCode, msgSeq); + if (!likedMsgId) { + this.core.context.logger.logError('解析表情回应消息失败: 未找到回应消息'); + } else { + this.emit( + 'group/emoji-like', + groupCode, + senderUin, + likedMsgId, + [{ emojiId, count: 1 }], + element.grayTipElement, msg, + ); + handled = true; + } + } + + // Todo: What is the temp id for group member increase? + try { + const groupCode = msg.peerUin; + const memberUin = element.grayTipElement.xmlElement.content.match(/uin="(\d+)"/)![1]; + const invitorUin = element.grayTipElement.xmlElement.content.match(/uin="(\d+)"/)![1]; + this.emit( + 'group/member-increase/invite', + groupCode, + memberUin, + invitorUin, + element.grayTipElement, msg, + ); + handled = true; + } catch (e) { + this.core.context.logger.logError('解析群邀请消息失败', e); + } + } + + if (element.grayTipElement.subElementType === NTGrayTipElementSubTypeV2.GRAYTIP_ELEMENT_SUBTYPE_JSON) { + /* + * Events that are included in jsonGrayTipElement: + * - group/poke + * - group/essence + * - group/title + */ + + const json = JSON.parse(element.grayTipElement.jsonGrayTipElement.jsonStr); + + try { + if (element.grayTipElement.jsonGrayTipElement.busiId === 1061) { + const pokeDetail = (json.items as any[]).filter(item => item.uid); + if (pokeDetail.length == 2) { + this.emit( + 'group/poke', + msg.peerUin, + await this.core.apis.UserApi.getUinByUidV2(pokeDetail[0].uid), + await this.core.apis.UserApi.getUinByUidV2(pokeDetail[1].uid)!, + element.grayTipElement, msg, + ); + } + handled = true; + } + } catch (e) { + this.core.context.logger.logError('解析群拍一拍消息失败', e); + } + + try { + if (element.grayTipElement.jsonGrayTipElement.busiId === 2401) { + const searchParams = new URL(json.items[0].jp).searchParams; + const msgSeq = searchParams.get('msgSeq')!; + const Group = searchParams.get('groupCode'); + const msgData = await this.core.apis.MsgApi.getMsgsBySeqAndCount({ + guildId: '', + chatType: ChatType.KCHATTYPEGROUP, + peerUid: Group!, + }, msgSeq.toString(), 1, true, true); + this.emit( + 'group/essence', + msg.peerUid, + msgData.msgList[0].msgId, + 'add', + element.grayTipElement, msg, + ); + handled = true; + } + } catch (e) { + this.core.context.logger.logError('解析群精华消息失败', e); + } + + try { + if (element.grayTipElement.jsonGrayTipElement.busiId === 2407) { + const memberUin = json.items[1].param[0]; + const title = json.items[3].txt; + this.emit( + 'group/title', + msg.peerUin, + memberUin, + title, + element.grayTipElement, msg, + ); + handled = true; + } + } catch (e) { + this.core.context.logger.logError('解析群头衔消息失败', e); + } + } + } + + if (element.fileElement) { + this.emit( + 'group/upload', + msg.peerUin, + msg.senderUin, + element.fileElement, + msg, + ); + } + } + } + + return handled; + } + private initBuddyListener() { const buddyListener = new NodeIKernelBuddyListener(); @@ -234,7 +537,7 @@ export class NapCatEventChannel extends continue; } this.emit( - 'group/member-decrease/kicked', + 'group/member-decrease/kick', notify.group.groupCode, leftMemberUin, operatorUin, @@ -320,4 +623,25 @@ export class NapCatEventChannel extends proxiedListenerOf(groupListener, this.core.context.logger), ); } + + private async findMsgIdForEmojiLikeEventByMsgSeq(groupCode: string, msgSeq: string) { + const peer = { + chatType: ChatType.KCHATTYPEGROUP, + guildId: '', + peerUid: groupCode, + }; + const replyMsgList = (await this.core.apis.MsgApi.getMsgExBySeq(peer, msgSeq)).msgList; + if (replyMsgList.length < 1) { + return null; + } + const replyMsg = replyMsgList + .filter(e => e.msgSeq == msgSeq) + .sort((a, b) => parseInt(a.msgTime) - parseInt(b.msgTime))[0]; + + if (!replyMsg) { + this.core.context.logger.logError('解析表情回应消息失败: 未找到回应消息'); + return null; + } + return replyMsg.msgId; + } }