import { GeneralCallResult, Group, GroupMember, GroupMemberRole, GroupRequestOperateTypes, InstanceContext, KickMemberV2Req, MemberExtSourceType, NapCatCore, } from '@/core'; import { isNumeric, sleep, solveAsyncProblem } from '@/common/helper'; import { LimitedHashTable } from '@/common/message-unique'; import { NTEventWrapper } from '@/common/event'; import { NapProtoMsg } from '../proto/NapProto'; import { OidbSvcTrpcTcpBase } from '../proto/oidb/OidbBase'; import { OidbSvcTrpcTcp0XED3_1 } from '../proto/oidb/Oidb.ed3_1'; interface recvPacket { type: string,//仅recv trace_id_md5?: string, data: { seq: number, hex_data: string, cmd: string } } export class NTQQGroupApi { context: InstanceContext; core: NapCatCore; groupCache: Map = new Map(); groupMemberCache: Map> = new Map>(); groups: Group[] = []; essenceLRU = new LimitedHashTable(1000); session: any; constructor(context: InstanceContext, core: NapCatCore) { this.context = context; this.core = core; this.initCache().then().catch(context.logger.logError.bind(context.logger)); } async initCache() { this.groups = await this.getGroups(); for (const group of this.groups) { this.groupCache.set(group.groupCode, group); } this.context.logger.logDebug(`加载${this.groups.length}个群组缓存完成`); // process.pid 调试点 } async getCoreAndBaseInfo(uids: string[]) { return await this.core.eventWrapper.callNoListenerEvent( 'NodeIKernelProfileService/getCoreAndBaseInfo', 'nodeStore', uids, ); } async sendPocketRkey() { let hex = '08E7A00210CA01221D0A130A05080110CA011206A80602B006011A0208022206080A081408022A006001'; let ret = await this.core.apis.PacketApi.sendPacket('OidbSvcTrpcTcp.0x9067_202', hex, true); //console.log('ret: ', ret); } async sendPacketPoke(group: number, peer: number) { let oidb_0xed3 = new NapProtoMsg(OidbSvcTrpcTcp0XED3_1).encode({ uin: peer, groupUin: group, friendUin: group, ext: 0 }); let oidb_packet = new NapProtoMsg(OidbSvcTrpcTcpBase).encode({ command: 0xed3, subCommand: 1, body: oidb_0xed3 }); let hex = Buffer.from(oidb_packet).toString('hex'); let retdata = await this.core.apis.PacketApi.sendPacket('OidbSvcTrpcTcp.0xed3_1', hex, false); } async fetchGroupEssenceList(groupCode: string) { const pskey = (await this.core.apis.UserApi.getPSkey(['qun.qq.com'])).domainPskeyMap.get('qun.qq.com')!; return this.context.session.getGroupService().fetchGroupEssenceList({ groupCode: groupCode, pageStart: 0, pageLimit: 300, }, pskey); } async clearGroupNotifiesUnreadCount(uk: boolean) { return this.context.session.getGroupService().clearGroupNotifiesUnreadCount(uk); } async setGroupAvatar(gc: string, filePath: string) { return this.context.session.getGroupService().setHeader(gc, filePath); } async getGroups(forced = false) { const [, , groupList] = await this.core.eventWrapper.callNormalEventV2( 'NodeIKernelGroupService/getGroupList', 'NodeIKernelGroupListener/onGroupListUpdate', [forced], ); return groupList; } async getGroupExtFE0Info(groupCode: string[], forced = true) { return this.context.session.getGroupService().getGroupExt0xEF0Info( groupCode, [], { bindGuildId: 1, blacklistExpireTime: 1, companyId: 1, essentialMsgPrivilege: 1, essentialMsgSwitch: 1, fullGroupExpansionSeq: 1, fullGroupExpansionSwitch: 1, gangUpId: 1, groupAioBindGuildId: 1, groupBindGuildIds: 1, groupBindGuildSwitch: 1, groupExcludeGuildIds: 1, groupExtFlameData: 1, groupFlagPro1: 1, groupInfoExtSeq: 1, groupOwnerId: 1, groupSquareSwitch: 1, hasGroupCustomPortrait: 1, inviteRobotMemberExamine: 1, inviteRobotMemberSwitch: 1, inviteRobotSwitch: 1, isLimitGroupRtc: 1, lightCharNum: 1, luckyWord: 1, luckyWordId: 1, msgEventSeq: 1, qqMusicMedalSwitch: 1, reserve: 1, showPlayTogetherSwitch: 1, starId: 1, todoSeq: 1, viewedMsgDisappearTime: 1, }, forced, ); } async getGroup(groupCode: string, forced = false) { let group = this.groupCache.get(groupCode.toString()); if (!group) { try { const groupList = await this.getGroups(forced); if (groupList.length) { groupList.forEach(g => { this.groupCache.set(g.groupCode, g); }); } } catch (e) { return undefined; } } group = this.groupCache.get(groupCode.toString()); return group; } async getGroupMemberAll(groupCode: string, forced = false) { return this.context.session.getGroupService().getAllMemberList(groupCode, forced); } async getGroupMember(groupCode: string | number, memberUinOrUid: string | number) { const groupCodeStr = groupCode.toString(); const memberUinOrUidStr = memberUinOrUid.toString(); let members = this.groupMemberCache.get(groupCodeStr); if (!members) { try { members = await this.getGroupMembersV2(groupCodeStr); // 更新群成员列表 this.groupMemberCache.set(groupCodeStr, members); } catch (e) { return null; } } // log('getGroupMember', members); function getMember() { let member: GroupMember | undefined; if (isNumeric(memberUinOrUidStr)) { member = Array.from(members!.values()).find(member => member.uin === memberUinOrUidStr); } else { member = members!.get(memberUinOrUidStr); } return member; } let member = getMember(); if (!member) { members = await this.getGroupMembersV2(groupCodeStr); member = getMember(); } return member; } async getGroupRecommendContactArkJson(groupCode: string) { return this.context.session.getGroupService().getGroupRecommendContactArkJson(groupCode); } async CreatGroupFileFolder(groupCode: string, folderName: string) { return this.context.session.getRichMediaService().createGroupFolder(groupCode, folderName); } async DelGroupFile(groupCode: string, files: string[]) { return this.context.session.getRichMediaService().deleteGroupFile(groupCode, [102], files); } async DelGroupFileFolder(groupCode: string, folderId: string) { return this.context.session.getRichMediaService().deleteGroupFolder(groupCode, folderId); } async addGroupEssence(GroupCode: string, msgId: string) { const MsgData = await this.context.session.getMsgService().getMsgsIncludeSelf({ chatType: 2, guildId: '', peerUid: GroupCode, }, msgId, 1, false); const param = { groupCode: GroupCode, msgRandom: parseInt(MsgData.msgList[0].msgRandom), msgSeq: parseInt(MsgData.msgList[0].msgSeq), }; return this.context.session.getGroupService().addGroupEssence(param); } async kickMemberV2Inner(param: KickMemberV2Req) { return this.context.session.getGroupService().kickMemberV2(param); } async deleteGroupBulletin(GroupCode: string, noticeId: string) { const psKey = (await this.core.apis.UserApi.getPSkey(['qun.qq.com'])).domainPskeyMap.get('qun.qq.com')!; return this.context.session.getGroupService().deleteGroupBulletin(GroupCode, psKey, noticeId); } async quitGroupV2(GroupCode: string, needDeleteLocalMsg: boolean) { const param = { groupCode: GroupCode, needDeleteLocalMsg: needDeleteLocalMsg, }; return this.context.session.getGroupService().quitGroupV2(param); } async removeGroupEssenceBySeq(GroupCode: string, msgRandom: string, msgSeq: string) { const param = { groupCode: GroupCode, msgRandom: parseInt(msgRandom), msgSeq: parseInt(msgSeq), }; return this.context.session.getGroupService().removeGroupEssence(param); } async removeGroupEssence(GroupCode: string, msgId: string) { const MsgData = await this.context.session.getMsgService().getMsgsIncludeSelf({ chatType: 2, guildId: '', peerUid: GroupCode, }, msgId, 1, false); const param = { groupCode: GroupCode, msgRandom: parseInt(MsgData.msgList[0].msgRandom), msgSeq: parseInt(MsgData.msgList[0].msgSeq), }; return this.context.session.getGroupService().removeGroupEssence(param); } async getSingleScreenNotifies(doubt: boolean, num: number) { const [, , , notifies] = await this.core.eventWrapper.callNormalEventV2( 'NodeIKernelGroupService/getSingleScreenNotifies', 'NodeIKernelGroupListener/onGroupSingleScreenNotifies', [ doubt, '', num, ], ); return notifies; } async getGroupMemberV2(GroupCode: string, uid: string, forced = false) { const Listener = this.core.eventWrapper.registerListen( 'NodeIKernelGroupListener/onMemberInfoChange', 1, forced ? 5000 : 250, (params, _, members) => params === GroupCode && members.size > 0, ); const retData = await ( this.core.eventWrapper .createEventFunction('NodeIKernelGroupService/getMemberInfo') )!(GroupCode, [uid], forced); if (retData.result !== 0) { throw new Error(`${retData.errMsg}`); } const result = await Listener as unknown; let member: GroupMember | undefined; if (Array.isArray(result) && result?.[2] instanceof Map) { const members = result[2] as Map; member = members.get(uid); } return member; } async searchGroup(groupCode: string) { const [, ret] = await this.core.eventWrapper.callNormalEventV2( 'NodeIKernelSearchService/searchGroup', 'NodeIKernelSearchListener/onSearchGroupResult', [{ keyWords: groupCode, groupNum: 25, exactSearch: false, penetrate: '' }], (ret) => ret.result === 0, (params) => !!params.groupInfos.find(g => g.groupCode === groupCode), 1, 5000 ); return ret.groupInfos.find(g => g.groupCode === groupCode); } async getGroupMemberEx(GroupCode: string, uid: string, forced = false, retry = 2) { const data = await solveAsyncProblem((eventWrapper: NTEventWrapper, GroupCode: string, uid: string, forced = false) => { return eventWrapper.callNormalEventV2( 'NodeIKernelGroupService/getMemberInfo', 'NodeIKernelGroupListener/onMemberInfoChange', [GroupCode, [uid], forced], (ret) => ret.result === 0, (params, _, members) => params === GroupCode && members.size > 0 && members.has(uid), 1, forced ? 2500 : 250 ); }, this.core.eventWrapper, GroupCode, uid, forced); if (data && data[3] instanceof Map && data[3].has(uid)) { return data[3].get(uid); } if (retry > 0) { const trydata = await this.getGroupMemberEx(GroupCode, uid, true, retry - 1) as GroupMember | undefined; if (trydata) return trydata; } return undefined; } async getGroupMembersV2(groupQQ: string, num = 3000): Promise> { const groupService = this.context.session.getGroupService(); const sceneId = groupService.createMemberListScene(groupQQ, 'groupMemberList_MainWindow'); const listener = this.core.eventWrapper.registerListen( 'NodeIKernelGroupListener/onMemberListChange', 1, 5000, (params) => params.sceneId === sceneId, ); try { const [membersFromFunc, membersFromListener] = await Promise.allSettled([ groupService.getNextMemberList(sceneId, undefined, num), listener, ]); if (membersFromFunc.status === 'fulfilled' && membersFromListener.status === 'fulfilled') { return new Map([ ...membersFromFunc.value.result.infos, ...membersFromListener.value[0].infos, ]); } if (membersFromFunc.status === 'fulfilled') { return membersFromFunc.value.result.infos; } if (membersFromListener.status === 'fulfilled') { return membersFromListener.value[0].infos; } throw new Error('获取群成员列表失败'); } finally { groupService.destroyMemberListScene(sceneId); } } async getGroupMembers(groupQQ: string, num = 3000): Promise> { const groupService = this.context.session.getGroupService(); const sceneId = groupService.createMemberListScene(groupQQ, 'groupMemberList_MainWindow'); const result = await groupService.getNextMemberList(sceneId!, undefined, num); if (result.errCode !== 0) { throw new Error('获取群成员列表出错,' + result.errMsg); } this.context.logger.logDebug(`获取群(${groupQQ})成员列表结果:`, `members: ${result.result.infos.size}`); return result.result.infos; } async getGroupFileCount(Gids: Array) { return this.context.session.getRichMediaService().batchGetGroupFileCount(Gids); } async getArkJsonGroupShare(GroupCode: string) { const ret = await this.core.eventWrapper.callNoListenerEvent( 'NodeIKernelGroupService/getGroupRecommendContactArkJson', GroupCode, ) as GeneralCallResult & { arkJson: string }; return ret.arkJson; } //需要异常处理 async uploadGroupBulletinPic(GroupCode: string, imageurl: string) { const _Pskey = (await this.core.apis.UserApi.getPSkey(['qun.qq.com'])).domainPskeyMap.get('qun.qq.com')!; return this.context.session.getGroupService().uploadGroupBulletinPic(GroupCode, _Pskey, imageurl); } async handleGroupRequest(flag: string, operateType: GroupRequestOperateTypes, reason?: string) { const flagitem = flag.split('|'); const groupCode = flagitem[0]; const seq = flagitem[1]; const type = parseInt(flagitem[2]); return this.context.session.getGroupService().operateSysNotify( false, { operateType: operateType, // 2 拒绝 targetMsg: { seq: seq, // 通知序列号 type: type, groupCode: groupCode, postscript: reason ?? ' ', // 仅传空值可能导致处理失败,故默认给个空格 }, }); } async quitGroup(groupQQ: string) { return this.context.session.getGroupService().quitGroup(groupQQ); } async kickMember(groupQQ: string, kickUids: string[], refuseForever: boolean = false, kickReason: string = '') { return this.context.session.getGroupService().kickMember(groupQQ, kickUids, refuseForever, kickReason); } async banMember(groupQQ: string, memList: Array<{ uid: string, timeStamp: number }>) { // timeStamp为秒数, 0为解除禁言 return this.context.session.getGroupService().setMemberShutUp(groupQQ, memList); } async banGroup(groupQQ: string, shutUp: boolean) { return this.context.session.getGroupService().setGroupShutUp(groupQQ, shutUp); } async setMemberCard(groupQQ: string, memberUid: string, cardName: string) { return this.context.session.getGroupService().modifyMemberCardName(groupQQ, memberUid, cardName); } async setMemberRole(groupQQ: string, memberUid: string, role: GroupMemberRole) { return this.context.session.getGroupService().modifyMemberRole(groupQQ, memberUid, role); } async setGroupName(groupQQ: string, groupName: string) { return this.context.session.getGroupService().modifyGroupName(groupQQ, groupName, false); } async publishGroupBulletin(groupQQ: string, content: string, picInfo: { id: string, width: number, height: number } | undefined = undefined, pinned: number = 0, confirmRequired: number = 0) { const psKey = (await this.core.apis.UserApi.getPSkey(['qun.qq.com'])).domainPskeyMap.get('qun.qq.com'); //text是content内容url编码 const data = { text: encodeURI(content), picInfo: picInfo, oldFeedsId: '', pinned: pinned, confirmRequired: confirmRequired, }; return this.context.session.getGroupService().publishGroupBulletin(groupQQ, psKey!, data); } async getGroupRemainAtTimes(GroupCode: string) { this.context.session.getGroupService().getGroupRemainAtTimes(GroupCode); } async getMemberExtInfo(groupCode: string, uin: string) { return this.context.session.getGroupService().getMemberExtInfo( { groupCode: groupCode, sourceType: MemberExtSourceType.TITLETYPE, beginUin: '0', dataTime: '0', uinList: [uin], uinNum: '', seq: '', groupType: '', richCardNameVer: '', memberExtFilter: { memberLevelInfoUin: 1, memberLevelInfoPoint: 1, memberLevelInfoActiveDay: 1, memberLevelInfoLevel: 1, memberLevelInfoName: 1, levelName: 1, dataTime: 1, userShowFlag: 1, sysShowFlag: 1, timeToUpdate: 1, nickName: 1, specialTitle: 1, levelNameNew: 1, userShowFlagNew: 1, msgNeedField: 1, cmdUinFlagExt3Grocery: 1, memberIcon: 1, memberInfoSeq: 1, }, }, ); } }