import { XMLParser } from 'fast-xml-parser' import { OB11Group, OB11GroupMember, OB11GroupMemberRole, OB11Message, OB11MessageData, OB11MessageDataType, OB11User, OB11UserSex, } from './types' import { AtType, ChatType, FaceIndex, GrayTipElementSubType, Group, Peer, GroupMember, RawMessage, Sex, TipGroupElementType, User, FriendV2, ChatType2 } from '../ntqqapi/types' import { EventType } from './event/OB11BaseEvent' import { encodeCQCode } from './cqcode' import { MessageUnique } from '../common/utils/messageUnique' import { OB11GroupIncreaseEvent } from './event/notice/OB11GroupIncreaseEvent' import { OB11GroupBanEvent } from './event/notice/OB11GroupBanEvent' import { OB11GroupUploadNoticeEvent } from './event/notice/OB11GroupUploadNoticeEvent' import { OB11GroupNoticeEvent } from './event/notice/OB11GroupNoticeEvent' import { calcQQLevel } from '../common/utils/misc' import { OB11GroupTitleEvent } from './event/notice/OB11GroupTitleEvent' import { OB11GroupCardEvent } from './event/notice/OB11GroupCardEvent' import { OB11GroupDecreaseEvent } from './event/notice/OB11GroupDecreaseEvent' import { OB11GroupMsgEmojiLikeEvent } from './event/notice/OB11MsgEmojiLikeEvent' import { OB11FriendAddNoticeEvent } from './event/notice/OB11FriendAddNoticeEvent' import { OB11FriendRecallNoticeEvent } from './event/notice/OB11FriendRecallNoticeEvent' import { OB11GroupRecallNoticeEvent } from './event/notice/OB11GroupRecallNoticeEvent' import { OB11FriendPokeEvent, OB11GroupPokeEvent } from './event/notice/OB11PokeEvent' import { OB11BaseNoticeEvent } from './event/notice/OB11BaseNoticeEvent' import { OB11GroupEssenceEvent } from './event/notice/OB11GroupEssenceEvent' import { omit, isNullable, pick, Dict } from 'cosmokit' import { Context } from 'cordis' import { selfInfo } from '@/common/globalVars' import { pathToFileURL } from 'node:url' import OneBot11Adapter from './adapter' export namespace OB11Entities { export async function message(ctx: Context, msg: RawMessage): Promise { const { debug, messagePostFormat, } = ctx.config as OneBot11Adapter.Config const selfUin = selfInfo.uin const resMsg: OB11Message = { self_id: parseInt(selfUin), user_id: parseInt(msg.senderUin!), time: parseInt(msg.msgTime) || Date.now(), message_id: msg.msgShortId!, real_id: msg.msgShortId!, message_seq: msg.msgShortId!, message_type: msg.chatType === ChatType.group ? 'group' : 'private', sender: { user_id: parseInt(msg.senderUin!), nickname: msg.sendNickName, card: msg.sendMemberName ?? '', }, raw_message: '', font: 14, sub_type: 'friend', message: messagePostFormat === 'string' ? '' : [], message_format: messagePostFormat === 'string' ? 'string' : 'array', post_type: selfUin == msg.senderUin ? EventType.MESSAGE_SENT : EventType.MESSAGE, } if (debug) { resMsg.raw = msg } if (msg.chatType === ChatType.group) { resMsg.sub_type = 'normal' resMsg.group_id = parseInt(msg.peerUin) const member = await ctx.ntGroupApi.getGroupMember(msg.peerUin, msg.senderUin!) if (member) { resMsg.sender.role = groupMemberRole(member.role) resMsg.sender.nickname = member.nick resMsg.sender.title = member.memberSpecialTitle ?? '' } } else if (msg.chatType === ChatType.friend) { resMsg.sub_type = 'friend' resMsg.sender.nickname = (await ctx.ntUserApi.getUserDetailInfo(msg.senderUid)).nick } else if (msg.chatType as unknown as ChatType2 === ChatType2.KCHATTYPETEMPC2CFROMGROUP) { resMsg.sub_type = 'group' resMsg.temp_source = 0 //群聊 resMsg.sender.nickname = (await ctx.ntUserApi.getUserDetailInfo(msg.senderUid)).nick const ret = await ctx.ntMsgApi.getTempChatInfo(ChatType2.KCHATTYPETEMPC2CFROMGROUP, msg.senderUid) if (ret?.result === 0) { resMsg.sender.group_id = Number(ret.tmpChatInfo?.groupCode) } else { resMsg.sender.group_id = 284840486 //兜底数据 } } for (const element of msg.elements) { let messageSegment: OB11MessageData | undefined if (element.textElement && element.textElement?.atType !== AtType.notAt) { let qq: string let name: string | undefined if (element.textElement.atType == AtType.atAll) { qq = 'all' } else { const { atNtUid, content } = element.textElement let atQQ = element.textElement.atUid if (!atQQ || atQQ === '0') { const atMember = await ctx.ntGroupApi.getGroupMember(msg.peerUin, atNtUid) if (atMember) { atQQ = atMember.uin } } if (atQQ) { qq = atQQ name = content.replace('@', '') } } messageSegment = { type: OB11MessageDataType.at, data: { qq: qq!, name } } } else if (element.textElement) { const text = element.textElement.content if (!text.trim()) { continue } messageSegment = { type: OB11MessageDataType.text, data: { text } } } else if (element.replyElement) { const { replyElement } = element const peer = { chatType: msg.chatType, peerUid: msg.peerUid, guildId: '' } try { const { replayMsgSeq, replyMsgTime, senderUidStr } = replyElement const records = msg.records.find(msgRecord => msgRecord.msgId === replyElement.sourceMsgIdInRecords) if (!records || !replyMsgTime || !senderUidStr) { throw new Error('找不到回复消息') } const { msgList } = await ctx.ntMsgApi.queryMsgsWithFilterExBySeq(peer, replayMsgSeq, replyMsgTime, [senderUidStr]) const replyMsg = msgList.find(msg => msg.msgRandom === records.msgRandom) // 284840486: 合并消息内侧 消息具体定位不到 if (!replyMsg && msg.peerUin !== '284840486') { ctx.logger.info('queryMsgs', msgList.map(e => pick(e, ['msgSeq', 'msgRandom']))) throw new Error('回复消息验证失败') } messageSegment = { type: OB11MessageDataType.reply, data: { id: MessageUnique.createMsg(peer, replyMsg ? replyMsg.msgId : records.msgId).toString() } } } catch (e: any) { ctx.logger.error('获取不到引用的消息', replyElement, e.stack) continue } } else if (element.picElement) { const { picElement } = element const fileSize = picElement.fileSize ?? '0' messageSegment = { type: OB11MessageDataType.image, data: { file: picElement.fileName, subType: picElement.picSubType, url: await ctx.ntFileApi.getImageUrl(picElement), file_size: fileSize, } } MessageUnique.addFileCache({ peerUid: msg.peerUid, msgId: msg.msgId, msgTime: +msg.msgTime, chatType: msg.chatType, elementId: element.elementId, elementType: element.elementType, fileName: picElement.fileName, fileUuid: picElement.fileUuid, fileSize, }) } else if (element.videoElement) { const { videoElement } = element const videoUrl = await ctx.ntFileApi.getVideoUrl({ chatType: msg.chatType, peerUid: msg.peerUid, }, msg.msgId, element.elementId) const fileSize = videoElement.fileSize ?? '0' messageSegment = { type: OB11MessageDataType.video, data: { file: videoElement.fileName, url: videoUrl || pathToFileURL(videoElement.filePath).href, path: videoElement.filePath, file_size: fileSize, } } MessageUnique.addFileCache({ peerUid: msg.peerUid, msgId: msg.msgId, msgTime: +msg.msgTime, chatType: msg.chatType, elementId: element.elementId, elementType: element.elementType, fileName: videoElement.fileName, fileUuid: videoElement.fileUuid!, fileSize, }) } else if (element.fileElement) { const { fileElement } = element const fileSize = fileElement.fileSize ?? '0' messageSegment = { type: OB11MessageDataType.file, data: { file: fileElement.fileName, url: pathToFileURL(fileElement.filePath).href, file_id: fileElement.fileUuid, path: fileElement.filePath, file_size: fileSize, } } MessageUnique.addFileCache({ peerUid: msg.peerUid, msgId: msg.msgId, msgTime: +msg.msgTime, chatType: msg.chatType, elementId: element.elementId, elementType: element.elementType, fileName: fileElement.fileName, fileUuid: fileElement.fileUuid!, fileSize, }) } else if (element.pttElement) { const { pttElement } = element const fileSize = pttElement.fileSize ?? '0' messageSegment = { type: OB11MessageDataType.voice, data: { file: pttElement.fileName, url: pathToFileURL(pttElement.filePath).href, path: pttElement.filePath, file_size: fileSize, } } MessageUnique.addFileCache({ peerUid: msg.peerUid, msgId: msg.msgId, msgTime: +msg.msgTime, chatType: msg.chatType, elementId: element.elementId, elementType: element.elementType, fileName: pttElement.fileName, fileUuid: pttElement.fileUuid, fileSize, }) } else if (element.arkElement) { const { arkElement } = element messageSegment = { type: OB11MessageDataType.json, data: { data: arkElement.bytesData } } } else if (element.faceElement) { const { faceElement } = element const faceId = faceElement.faceIndex if (faceId === FaceIndex.dice) { messageSegment = { type: OB11MessageDataType.dice, data: { result: faceElement.resultId! } } } else if (faceId === FaceIndex.RPS) { messageSegment = { type: OB11MessageDataType.RPS, data: { result: faceElement.resultId! } } } else { messageSegment = { type: OB11MessageDataType.face, data: { id: faceId.toString() } } } } else if (element.marketFaceElement) { const { marketFaceElement } = element const { emojiId } = marketFaceElement // 取md5的前两位 const dir = emojiId.substring(0, 2) // 获取组装url // const url = `https://p.qpic.cn/CDN_STATIC/0/data/imgcache/htdocs/club/item/parcel/item/${dir}/${md5}/300x300.gif?max_age=31536000` const url = `https://gxh.vip.qq.com/club/item/parcel/item/${dir}/${emojiId}/raw300.gif` messageSegment = { type: OB11MessageDataType.mface, data: { summary: marketFaceElement.faceName!, url, emoji_id: emojiId, emoji_package_id: marketFaceElement.emojiPackageId, key: marketFaceElement.key } } //mFaceCache.set(emojiId, element.marketFaceElement.faceName!) } else if (element.markdownElement) { const { markdownElement } = element messageSegment = { type: OB11MessageDataType.markdown, data: { data: markdownElement.content } } } else if (element.multiForwardMsgElement) { messageSegment = { type: OB11MessageDataType.forward, data: { id: msg.msgId } } } if (messageSegment) { const cqCode = encodeCQCode(messageSegment) if (messagePostFormat === 'string') { (resMsg.message as string) += cqCode } else { (resMsg.message as OB11MessageData[]).push(messageSegment) } resMsg.raw_message += cqCode } } resMsg.raw_message = resMsg.raw_message.trim() return resMsg } export async function privateEvent(ctx: Context, msg: RawMessage): Promise { if (msg.chatType !== ChatType.friend) { return } for (const element of msg.elements) { if (element.grayTipElement) { if (element.grayTipElement.subElementType == GrayTipElementSubType.MEMBER_NEW_TITLE) { const json = JSON.parse(element.grayTipElement.jsonGrayTipElement.jsonStr) if (element.grayTipElement.jsonGrayTipElement.busiId == 1061) { //判断业务类型 //Poke事件 const pokedetail: any[] = json.items //筛选item带有uid的元素 const poke_uid = pokedetail.filter(item => item.uid) if (poke_uid.length == 2) { return new OB11FriendPokeEvent( parseInt(await ctx.ntUserApi.getUinByUid(poke_uid[0].uid)), parseInt(await ctx.ntUserApi.getUinByUid(poke_uid[1].uid)), pokedetail ) } } //下面得改 上面也是错的grayTipElement.subElementType == GrayTipElementSubType.MEMBER_NEW_TITLE } } } // 好友增加事件 if (msg.msgType === 5 && msg.subMsgType === 12) { const event = new OB11FriendAddNoticeEvent(parseInt(msg.peerUin)) return event } } export async function groupEvent(ctx: Context, msg: RawMessage): Promise { if (msg.chatType !== ChatType.group) { return } if (msg.senderUin) { const member = await ctx.ntGroupApi.getGroupMember(msg.peerUid, msg.senderUin) if (member && member.cardName !== msg.sendMemberName) { const event = new OB11GroupCardEvent( parseInt(msg.peerUid), parseInt(msg.senderUin), msg.sendMemberName!, member.cardName, ) member.cardName = msg.sendMemberName! return event } } for (const element of msg.elements) { const grayTipElement = element.grayTipElement const groupElement = grayTipElement?.groupElement if (groupElement) { if (groupElement.type === TipGroupElementType.memberIncrease) { ctx.logger.info('收到群成员增加消息', groupElement) await ctx.sleep(1000) const member = await ctx.ntGroupApi.getGroupMember(msg.peerUid, groupElement.memberUid) let memberUin = member?.uin if (!memberUin) { memberUin = (await ctx.ntUserApi.getUserDetailInfo(groupElement.memberUid)).uin } const adminMember = await ctx.ntGroupApi.getGroupMember(msg.peerUid, groupElement.adminUid) if (memberUin) { const operatorUin = adminMember?.uin || memberUin return new OB11GroupIncreaseEvent(parseInt(msg.peerUid), parseInt(memberUin), parseInt(operatorUin)) } } else if (groupElement.type === TipGroupElementType.ban) { ctx.logger.info('收到群群员禁言提示', groupElement) const memberUid = groupElement.shutUp?.member.uid const adminUid = groupElement.shutUp?.admin.uid let memberUin: string = '' let duration = Number(groupElement.shutUp?.duration) const subType = duration > 0 ? 'ban' : 'lift_ban' if (memberUid) { memberUin = (await ctx.ntGroupApi.getGroupMember(msg.peerUid, memberUid))?.uin || (await ctx.ntUserApi.getUserDetailInfo(memberUid))?.uin } else { memberUin = '0' // 0表示全员禁言 if (duration > 0) { duration = -1 } } const adminUin = (await ctx.ntGroupApi.getGroupMember(msg.peerUid, adminUid!))?.uin || (await ctx.ntUserApi.getUserDetailInfo(adminUid!))?.uin if (memberUin && adminUin) { return new OB11GroupBanEvent( parseInt(msg.peerUid), parseInt(memberUin), parseInt(adminUin), duration, subType, ) } } else if (groupElement.type === TipGroupElementType.kicked) { ctx.logger.info(`收到我被踢出或退群提示, 群${msg.peerUid}`, groupElement) ctx.ntGroupApi.quitGroup(msg.peerUid) try { const adminUin = (await ctx.ntGroupApi.getGroupMember(msg.peerUid, groupElement.adminUid))?.uin || (await ctx.ntUserApi.getUinByUid(groupElement.adminUid)) if (adminUin) { return new OB11GroupDecreaseEvent( parseInt(msg.peerUid), parseInt(selfInfo.uin), parseInt(adminUin), 'kick_me' ) } } catch (e) { return new OB11GroupDecreaseEvent( parseInt(msg.peerUid), parseInt(selfInfo.uin), 0, 'leave' ) } } } else if (element.fileElement) { return new OB11GroupUploadNoticeEvent(parseInt(msg.peerUid), parseInt(msg.senderUin!), { id: element.fileElement.fileUuid!, name: element.fileElement.fileName, size: parseInt(element.fileElement.fileSize), busid: element.fileElement.fileBizId || 0, }) } if (grayTipElement) { const xmlElement = grayTipElement.xmlElement if (xmlElement?.templId === '10382') { // 表情回应消息 // "content": // " // // // // // ", const emojiLikeData = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: '', }).parse(xmlElement.content) ctx.logger.info('收到表情回应我的消息', emojiLikeData) try { const senderUin: string = emojiLikeData.gtip.qq.jp const msgSeq: string = emojiLikeData.gtip.url.msgseq const emojiId: string = emojiLikeData.gtip.face.id const peer = { chatType: ChatType.group, guildId: '', peerUid: msg.peerUid, } const replyMsgList = (await ctx.ntMsgApi.queryFirstMsgBySeq(peer, msgSeq)).msgList if (!replyMsgList?.length) { return } const shortId = MessageUnique.getShortIdByMsgId(replyMsgList[0].msgId) return new OB11GroupMsgEmojiLikeEvent( parseInt(msg.peerUid), parseInt(senderUin), shortId!, [{ emoji_id: emojiId, count: 1, }] ) } catch (e) { ctx.logger.error('解析表情回应消息失败', (e as Error).stack) } } if ( grayTipElement.subElementType == GrayTipElementSubType.INVITE_NEW_MEMBER && xmlElement?.templId == '10179' ) { ctx.logger.info('收到新人被邀请进群消息', grayTipElement) if (xmlElement?.content) { const regex = /jp="(\d+)"/g const matches: string[] = [] let match: RegExpExecArray | null = null while ((match = regex.exec(xmlElement.content)) !== null) { matches.push(match[1]) } // log("新人进群匹配到的QQ号", matches) if (matches.length === 2) { const [inviter, invitee] = matches return new OB11GroupIncreaseEvent(parseInt(msg.peerUid), parseInt(invitee), parseInt(inviter), 'invite') } } } else if (grayTipElement.subElementType == GrayTipElementSubType.MEMBER_NEW_TITLE) { const json = JSON.parse(grayTipElement.jsonGrayTipElement.jsonStr) /* { align: 'center', items: [ { txt: '恭喜', type: 'nor' }, { col: '3', jp: '5', param: ["QQ号"], txt: '林雨辰', type: 'url' }, { txt: '获得群主授予的', type: 'nor' }, { col: '3', jp: '', txt: '好好好', type: 'url' }, { txt: '头衔', type: 'nor' } ] } * */ if (grayTipElement.jsonGrayTipElement.busiId == 1061) { //判断业务类型 //Poke事件 const pokedetail: Dict[] = json.items //筛选item带有uid的元素 const poke_uid = pokedetail.filter(item => item.uid) if (poke_uid.length == 2) { return new OB11GroupPokeEvent( parseInt(msg.peerUid), parseInt(await ctx.ntUserApi.getUinByUid(poke_uid[0].uid) ?? 0), parseInt(await ctx.ntUserApi.getUinByUid(poke_uid[1].uid) ?? 0), pokedetail ) } } if (grayTipElement.jsonGrayTipElement.busiId == 2401) { ctx.logger.info('收到群精华消息', json) const searchParams = new URL(json.items[0].jp).searchParams const msgSeq = searchParams.get('msgSeq')! const Group = searchParams.get('groupCode') const Peer: Peer = { guildId: '', chatType: ChatType.group, peerUid: Group! } const msgList = (await ctx.ntMsgApi.getMsgsBySeqAndCount(Peer, msgSeq.toString(), 1, true, true))?.msgList if (!msgList?.length) { return } //const origMsg = await dbUtil.getMsgByLongId(msgList[0].msgId) //const postMsg = await dbUtil.getMsgBySeqId(origMsg?.msgSeq!) ?? origMsg // 如果 senderUin 为 0,可能是 历史消息 或 自身消息 //if (msgList[0].senderUin === '0') { //msgList[0].senderUin = postMsg?.senderUin ?? getSelfUin() //} return new OB11GroupEssenceEvent( parseInt(msg.peerUid), MessageUnique.getShortIdByMsgId(msgList[0].msgId)!, parseInt(msgList[0].senderUin!) ) // 获取MsgSeq+Peer可获取具体消息 } if (grayTipElement.jsonGrayTipElement.busiId == 2407) { const memberUin = json.items[1].param[0] const title = json.items[3].txt ctx.logger.info('收到群成员新头衔消息', json) ctx.ntGroupApi.getGroupMember(msg.peerUid, memberUin).then(member => { if (!isNullable(member)) { member.memberSpecialTitle = title } }) return new OB11GroupTitleEvent(parseInt(msg.peerUid), parseInt(memberUin), title) } } } } } export async function recallEvent( ctx: Context, msg: RawMessage, shortId: number ): Promise { const msgElement = msg.elements.find( (element) => element.grayTipElement?.subElementType === GrayTipElementSubType.RECALL, ) if (!msgElement) { return } const revokeElement = msgElement.grayTipElement.revokeElement if (msg.chatType === ChatType.group) { const operator = await ctx.ntGroupApi.getGroupMember(msg.peerUid, revokeElement.operatorUid) return new OB11GroupRecallNoticeEvent( parseInt(msg.peerUid), parseInt(msg.senderUin!), parseInt(operator?.uin || msg.senderUin!), shortId, ) } else { return new OB11FriendRecallNoticeEvent(parseInt(msg.senderUin!), shortId) } } export function friend(friend: User): OB11User { return { user_id: parseInt(friend.uin), nickname: friend.nick, remark: friend.remark, sex: sex(friend.sex!), level: (friend.qqLevel && calcQQLevel(friend.qqLevel)) || 0, } } export function friends(friends: User[]): OB11User[] { return friends.map(friend) } export function friendsV2(friends: FriendV2[]): OB11User[] { const data: OB11User[] = [] for (const friend of friends) { const sexValue = sex(friend.baseInfo.sex!) data.push({ ...omit(friend.baseInfo, ['richBuffer']), ...friend.coreInfo, user_id: parseInt(friend.coreInfo.uin), nickname: friend.coreInfo.nick, remark: friend.coreInfo.nick, sex: sexValue, level: 0, categroyName: friend.categroyName, categoryId: friend.categoryId }) } return data } export function groupMemberRole(role: number): OB11GroupMemberRole | undefined { return { 4: OB11GroupMemberRole.owner, 3: OB11GroupMemberRole.admin, 2: OB11GroupMemberRole.member, }[role] } export function sex(sex: Sex): OB11UserSex { const sexMap = { [Sex.male]: OB11UserSex.male, [Sex.female]: OB11UserSex.female, [Sex.unknown]: OB11UserSex.unknown, } return sexMap[sex] || OB11UserSex.unknown } export function groupMember(group_id: string, member: GroupMember): OB11GroupMember { return { group_id: parseInt(group_id), user_id: parseInt(member.uin), nickname: member.nick, card: member.cardName, sex: sex(member.sex!), age: 0, area: '', level: '0', qq_level: (member.qqLevel && calcQQLevel(member.qqLevel)) || 0, join_time: 0, // 暂时没法获取 last_sent_time: 0, // 暂时没法获取 title_expire_time: 0, unfriendly: false, card_changeable: true, is_robot: member.isRobot, shut_up_timestamp: member.shutUpTime, role: groupMemberRole(member.role), title: member.memberSpecialTitle || '', } } export function stranger(user: User): OB11User { return { ...user, user_id: parseInt(user.uin), nickname: user.nick, sex: sex(user.sex!), age: 0, qid: user.qid, login_days: 0, level: (user.qqLevel && calcQQLevel(user.qqLevel)) || 0, } } export function group(group: Group): OB11Group { return { group_id: parseInt(group.groupCode), group_name: group.groupName, group_memo: group.remarkName, group_create_time: +group.createTime, member_count: group.memberCount, max_member_count: group.maxMember, } } export function groups(groups: Group[]): OB11Group[] { return groups.map(group) } }