diff --git a/src/core/apis/collection.ts b/src/core/apis/collection.ts new file mode 100644 index 00000000..bcaf8795 --- /dev/null +++ b/src/core/apis/collection.ts @@ -0,0 +1,53 @@ +import { napCatCore } from ".."; + +export class NTQQCollectionApi { + static async createCollection(authorUin: string, authorUid: string, authorName: string, brief: string, rawData: string) { + let param = { + commInfo: { + bid: 1, + category: 2, + author: { + type: 1, + numId: authorUin, + strId: authorName, + groupId: '0', + groupName: '', + uid: authorUid + }, + customGroupId: '0', + createTime: Date.now().toString(), + sequence: Date.now().toString() + }, + richMediaSummary: { + originalUri: '', + publisher: '', + richMediaVersion: 0, + subTitle: '', + title: '', + brief: brief, + picList: [], + contentType: 1 + }, + richMediaContent: { + rawData: rawData, + bizDataList: [], + picList: [], + fileList: [] + }, + need_share_url: false + }; + return napCatCore.session.getCollectionService().createNewCollectionItem(param); + } + static async getAllCollection(category: number = 0, count: number = 50) { + let param = { + category: category, + groupId: -1, + forceSync: true, + forceFromDb: false, + timeStamp: "0", + count: count, + searchDown: true + }; + return napCatCore.session.getCollectionService().getCollectionItemList(param); + } +} diff --git a/src/core/apis/file.ts b/src/core/apis/file.ts new file mode 100644 index 00000000..4564d4da --- /dev/null +++ b/src/core/apis/file.ts @@ -0,0 +1,341 @@ +import { + CacheFileListItem, + CacheFileType, + ChatCacheListItemBasic, + ChatType, + ElementType, IMAGE_HTTP_HOST, IMAGE_HTTP_HOST_NT, Peer, PicElement, RawMessage +} from '@/core/entities'; +import path from 'path'; +import fs from 'fs'; +import fsPromises from 'fs/promises'; +import { log, logDebug, logError } from '@/common/utils/log'; +import { GeneralCallResult, napCatCore, OnRichMediaDownloadCompleteParams } from '@/core'; +import { calculateFileMD5 } from '@/common/utils/file'; +import * as fileType from 'file-type'; +import imageSize from 'image-size'; +import { ISizeCalculationResult } from 'image-size/dist/types/interface'; +import { sessionConfig } from '@/core/sessionConfig'; +import { rkeyManager } from '../utils/rkey'; +import { NTEventDispatch } from '@/common/utils/EventTask'; +import { NodeIKernelSearchService } from '../services/NodeIKernelSearchService'; +import { selfInfo } from '../data'; + + +export class NTQQFileApi { + static async getFileType(filePath: string) { + return fileType.fileTypeFromFile(filePath); + } + + static async copyFile(filePath: string, destPath: string) { + await napCatCore.util.copyFile(filePath, destPath); + } + + static async getFileSize(filePath: string): Promise { + return await napCatCore.util.getFileSize(filePath); + } + static async getVideoUrl(peer: Peer, msgId: string, elementId: string) { + return (await napCatCore.session.getRichMediaService().getVideoPlayUrlV2(peer, msgId, elementId, 0, { downSourceType: 1, triggerType: 1 })).urlResult.domainUrl; + } + // 上传文件到QQ的文件夹 + static async uploadFile(filePath: string, elementType: ElementType = ElementType.PIC, elementSubType: number = 0) { + // napCatCore.wrapper.util. + const fileMd5 = await calculateFileMD5(filePath); + let ext: string = (await NTQQFileApi.getFileType(filePath))?.ext as string || ''; + if (ext) { + ext = '.' + ext; + } + let fileName = `${path.basename(filePath)}`; + if (fileName.indexOf('.') === -1) { + fileName += ext; + } + const mediaPath = napCatCore.session.getMsgService().getRichMediaFilePathForGuild({ + md5HexStr: fileMd5, + fileName: fileName, + elementType: elementType, + elementSubType, + thumbSize: 0, + needCreate: true, + downloadType: 1, + file_uuid: '' + }); + await NTQQFileApi.copyFile(filePath, mediaPath!); + const fileSize = await NTQQFileApi.getFileSize(filePath); + return { + md5: fileMd5, + fileName, + path: mediaPath, + fileSize, + ext + }; + } + static async downloadMediaByUuid() { + //napCatCore.session.getRichMediaService().downloadFileForFileUuid(); + } + static async downloadMedia(msgId: string, chatType: ChatType, peerUid: string, elementId: string, thumbPath: string, sourcePath: string, timeout = 1000 * 60 * 2, force: boolean = false) { + //logDebug('receive downloadMedia task', msgId, chatType, peerUid, elementId, thumbPath, sourcePath, timeout, force); + // 用于下载收到的消息中的图片等 + if (sourcePath && fs.existsSync(sourcePath)) { + if (force) { + try { + await fsPromises.unlink(sourcePath); + } catch (e) { + // + } + } else { + return sourcePath; + } + } + let data = await NTEventDispatch.CallNormalEvent< + ( + params: { + fileModelId: string, + downloadSourceType: number, + triggerType: number, + msgId: string, + chatType: ChatType, + peerUid: string, + elementId: string, + thumbSize: number, + downloadType: number, + filePath: string + }) => Promise, + (fileTransNotifyInfo: OnRichMediaDownloadCompleteParams) => void + >( + 'NodeIKernelMsgService/downloadRichMedia', + 'NodeIKernelMsgListener/onRichMediaDownloadComplete', + 1, + timeout, + (arg: OnRichMediaDownloadCompleteParams) => { + if (arg.msgId === msgId) { + return true; + } + return false; + }, + { + fileModelId: '0', + downloadSourceType: 0, + triggerType: 1, + msgId: msgId, + chatType: chatType, + peerUid: peerUid, + elementId: elementId, + thumbSize: 0, + downloadType: 1, + filePath: thumbPath + } + ); + let filePath = data[1].filePath; + if (filePath.startsWith('\\')) { + // log('filePath start with \\'); + const downloadPath = sessionConfig.defaultFileDownloadPath; + //logDebug('downloadPath', downloadPath); + filePath = path.join(downloadPath, filePath); + // 下载路径是下载文件夹的相对路径 + } + return filePath; + } + + static async getImageSize(filePath: string): Promise { + return new Promise((resolve, reject) => { + imageSize(filePath, (err, dimensions) => { + if (err) { + reject(err); + } else { + resolve(dimensions); + } + }); + }); + } + static async addFileCache(peer: Peer, msgId: string, msgSeq: string, senderUid: string, elemId: string, elemType: string, fileSize: string, fileName: string) { + let GroupData; + let BuddyData; + if (peer.chatType === ChatType.group) { + GroupData = + [{ + groupCode: peer.peerUid, + isConf: false, + hasModifyConfGroupFace: true, + hasModifyConfGroupName: true, + groupName: "NapCat.Cached", + remark: "NapCat.Cached" + }]; + } else if (peer.chatType === ChatType.friend) { + BuddyData = [{ + category_name: 'NapCat.Cached', + peerUid: peer.peerUid, + peerUin: peer.peerUid, + remark: 'NapCat.Cached' + }]; + } else { + return undefined; + } + + return napCatCore.session.getSearchService().addSearchHistory({ + type: 4, + contactList: [], + id: -1, + groupInfos: [], + msgs: [], + fileInfos: [ + { + chatType: peer.chatType, + buddyChatInfo: BuddyData || [], + discussChatInfo: [], + groupChatInfo: GroupData || [], + dataLineChatInfo: [], + tmpChatInfo: [], + msgId: msgId, + msgSeq: msgSeq, + msgTime: Math.floor(Date.now() / 1000).toString(), + senderUid: senderUid, + senderNick: 'NapCat.Cached', + senderRemark: 'NapCat.Cached', + senderCard: 'NapCat.Cached', + elemId: elemId, + elemType: elemType, + fileSize: fileSize, + filePath: '', + fileName: fileName, + hits: [{ + start: 12, + end: 14 + }] + } + ] + }); + } + static async searchfile(keys: string[]) { + type EventType = NodeIKernelSearchService['searchFileWithKeywords']; + interface OnListener { + searchId: string, + hasMore: boolean, + resultItems: { + chatType: ChatType, + buddyChatInfo: any[], + discussChatInfo: any[], + groupChatInfo: + { + groupCode: string, + isConf: boolean, + hasModifyConfGroupFace: boolean, + hasModifyConfGroupName: boolean, + groupName: string, + remark: string + }[] + , + dataLineChatInfo: any[], + tmpChatInfo: any[], + msgId: string, + msgSeq: string, + msgTime: string, + senderUid: string, + senderNick: string, + senderRemark: string, + senderCard: string, + elemId: string, + elemType: number, + fileSize: string, + filePath: string, + fileName: string, + hits: + { + start: number, + end: number + }[] + }[] + }; + const Event = await NTEventDispatch.CreatEventFunction('NodeIKernelSearchService/searchFileWithKeywords'); + let id = ''; + const Listener = NTEventDispatch.RegisterListen<(params: OnListener) => void>('NodeIKernelSearchListener/onSearchFileKeywordsResult', 1, 20000, (params) => { + if (id !== '' && params.searchId == id) { + return true + } + return false; + }); + id = await Event!(keys, 12); + let [ret] = (await Listener); + return ret; + } + static async getImageUrl(element: PicElement) { + if (!element) { + return ''; + } + const url: string = element.originImageUrl!; // 没有域名 + const md5HexStr = element.md5HexStr; + const fileMd5 = element.md5HexStr; + const fileUuid = element.fileUuid; + + if (url) { + let UrlParse = new URL(IMAGE_HTTP_HOST + url);//临时解析拼接 + let imageAppid = UrlParse.searchParams.get('appid'); + let isNewPic = imageAppid && ['1406', '1407'].includes(imageAppid); + if (isNewPic) { + let UrlRkey = UrlParse.searchParams.get('rkey'); + if (UrlRkey) { + return IMAGE_HTTP_HOST_NT + url; + } + const rkeyData = await rkeyManager.getRkey(); + UrlRkey = imageAppid === '1406' ? rkeyData.private_rkey : rkeyData.group_rkey; + return IMAGE_HTTP_HOST_NT + url + `${UrlRkey}`; + } else { + // 老的图片url,不需要rkey + return IMAGE_HTTP_HOST + url; + } + } else if (fileMd5 || md5HexStr) { + // 没有url,需要自己拼接 + return `${IMAGE_HTTP_HOST}/gchatpic_new/0/0-0-${(fileMd5 || md5HexStr)!.toUpperCase()}/0`; + } + logDebug('图片url获取失败', element); + return ''; + } +} + +export class NTQQFileCacheApi { + static async setCacheSilentScan(isSilent: boolean = true) { + return ''; + } + + static getCacheSessionPathList() { + return ''; + } + + static clearCache(cacheKeys: Array = ['tmp', 'hotUpdate']) { + // 参数未验证 + return napCatCore.session.getStorageCleanService().clearCacheDataByKeys(cacheKeys); + } + + static addCacheScannedPaths(pathMap: object = {}) { + return napCatCore.session.getStorageCleanService().addCacheScanedPaths(pathMap); + } + + static scanCache(): Promise { + // 需要注册Listener onFinishScan + return napCatCore.session.getStorageCleanService().scanCache(); + } + + static getHotUpdateCachePath() { + // 未实现 + return ''; + } + + static getDesktopTmpPath() { + // 未实现 + return ''; + } + + static getChatCacheList(type: ChatType, pageSize: number = 1000, pageIndex: number = 0) { + return napCatCore.session.getStorageCleanService().getChatCacheInfo(type, pageSize, 1, pageIndex); + } + + static getFileCacheInfo(fileType: CacheFileType, pageSize: number = 1000, lastRecord?: CacheFileListItem) { + const _lastRecord = lastRecord ? lastRecord : { fileType: fileType }; + //需要五个参数 + //return napCatCore.session.getStorageCleanService().getFileCacheInfo(); + } + + static async clearChatCache(chats: ChatCacheListItemBasic[] = [], fileKeys: string[] = []) { + return napCatCore.session.getStorageCleanService().clearChatCacheInfo(chats, fileKeys); + } +} diff --git a/src/core/apis/friend.ts b/src/core/apis/friend.ts new file mode 100644 index 00000000..d9f853fe --- /dev/null +++ b/src/core/apis/friend.ts @@ -0,0 +1,97 @@ +import { FriendRequest, FriendV2, SimpleInfo, User } from '@/core/entities'; +import { BuddyListReqType, napCatCore, NodeIKernelBuddyListener, NodeIKernelProfileService, OnBuddyChangeParams } from '@/core'; +import { NTEventDispatch } from '@/common/utils/EventTask'; +import { LimitedHashTable } from '@/common/utils/MessageUnique'; +import { CacheClassFuncAsyncExtend } from '@/common/utils/helper'; +export class NTQQFriendApi { + static async getBuddyV2(refresh = false): Promise { + let uids: string[] = []; + const buddyService = napCatCore.session.getBuddyService(); + const buddyListV2 = refresh ? await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL) : await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL); + uids.push(...buddyListV2.data.flatMap(item => item.buddyUids)); + const data = await NTEventDispatch.CallNoListenerEvent( + 'NodeIKernelProfileService/getCoreAndBaseInfo', 5000, 'nodeStore', uids + ); + return Array.from(data.values()); + } + @CacheClassFuncAsyncExtend(3600 * 1000, 'getBuddyIdMap', () => true) + static async getBuddyIdMapCache(refresh = false): Promise> { + return await NTQQFriendApi.getBuddyIdMap(refresh); + } + static async getBuddyIdMap(refresh = false): Promise> { + let uids: string[] = []; + let retMap: LimitedHashTable = new LimitedHashTable(5000); + const buddyService = napCatCore.session.getBuddyService(); + const buddyListV2 = refresh ? await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL) : await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL); + uids.push(...buddyListV2.data.flatMap(item => item.buddyUids)); + const data = await NTEventDispatch.CallNoListenerEvent( + 'NodeIKernelProfileService/getCoreAndBaseInfo', 5000, 'nodeStore', uids + ); + data.forEach((value, key) => { + retMap.set(value.uin!, value.uid!); + }); + //console.log('getBuddyIdMap', retMap.getValue); + return retMap; + } + static async getBuddyV2ExWithCate(refresh = false) { + let uids: string[] = []; + let categoryMap: Map = new Map(); + const buddyService = napCatCore.session.getBuddyService(); + const buddyListV2 = refresh ? (await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL)).data : (await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL)).data; + uids.push( + ...buddyListV2.flatMap(item => { + item.buddyUids.forEach(uid => { + categoryMap.set(uid, { categoryId: item.categoryId, categroyName: item.categroyName }); + }); + return item.buddyUids + })); + const data = await NTEventDispatch.CallNoListenerEvent( + 'NodeIKernelProfileService/getCoreAndBaseInfo', 5000, 'nodeStore', uids + ); + return Array.from(data).map(([key, value]) => { + const category = categoryMap.get(key); + return category ? { ...value, categoryId: category.categoryId, categroyName: category.categroyName } : value; + }); + } + static async isBuddy(uid: string) { + return napCatCore.session.getBuddyService().isBuddy(uid); + } + /** + * @deprecated + * @param forced + * @returns + */ + static async getFriends(forced = false): Promise { + let [_retData, _BuddyArg] = await NTEventDispatch.CallNormalEvent + <(force: boolean) => Promise, (arg: OnBuddyChangeParams) => void> + ( + 'NodeIKernelBuddyService/getBuddyList', + 'NodeIKernelBuddyListener/onBuddyListChange', + 1, + 5000, + () => true, + forced + ); + const friends: User[] = []; + for (const categoryItem of _BuddyArg) { + for (const friend of categoryItem.buddyList) { + friends.push(friend); + } + } + return friends; + } + + static async handleFriendRequest(flag: string, accept: boolean) { + let data = flag.split('|'); + if (data.length < 2) { + return; + } + let friendUid = data[0]; + let reqTime = data[1]; + napCatCore.session.getBuddyService()?.approvalFriendRequest({ + friendUid: friendUid, + reqTime: reqTime, + accept + }); + } +} diff --git a/src/core/apis/group.ts b/src/core/apis/group.ts new file mode 100644 index 00000000..1ccf1e99 --- /dev/null +++ b/src/core/apis/group.ts @@ -0,0 +1,327 @@ +import { GroupMember, GroupRequestOperateTypes, GroupMemberRole, GroupNotify, Group, MemberExtSourceType, GroupNotifyTypes, ChatType, Peer, GroupListUpdateType } from '../entities'; +import { GeneralCallResult, NTQQUserApi, NodeIKernelGroupListener, NodeIKernelGroupService, napCatCore } from '@/core'; +import { NTEventDispatch } from '@/common/utils/EventTask'; +import { log } from '@/common/utils/log'; +import { groupMembers } from '../data'; +import { CacheClassFuncAsyncExtend, runAllWithTimeout } from '@/common/utils/helper'; +export class NTQQGroupApi { + static async setGroupAvatar(gc: string, filePath: string) { + return napCatCore.session.getGroupService().setHeader(gc, filePath); + } + static async getGroups(forced = false) { + type ListenerType = NodeIKernelGroupListener['onGroupListUpdate']; + let [_retData, _updateType, groupList] = await NTEventDispatch.CallNormalEvent + <(force: boolean) => Promise, ListenerType> + ( + 'NodeIKernelGroupService/getGroupList', + 'NodeIKernelGroupListener/onGroupListUpdate', + 1, + 5000, + (updateType) => true, + forced + ); + return groupList; + } + + @CacheClassFuncAsyncExtend(3600 * 1000, "LastestSendTime", () => true) + static async getGroupMemberLastestSendTimeCache(GroupCode: string) { + return NTQQGroupApi.getGroupMemberLastestSendTime(GroupCode); + } + /** + * 通过QQ自带数据库获取群成员最后发言时间(仅返回有效数据 且消耗延迟大 需要进行缓存) + * @param GroupCode 群号 + * @returns Map key: uin value: sendTime + * @example + * let ret = await NTQQGroupApi.getGroupMemberLastestSendTime('123456'); + * for (let [uin, sendTime] of ret) { + * console.log(uin, sendTime); + * } + */ + static async getGroupMemberLastestSendTime(GroupCode: string) { + async function getdata(uid: string) { + let NTRet = await NTQQGroupApi.getLastestMsgByUids(GroupCode, [uid]); + if (NTRet.result != 0 && NTRet.msgList.length < 1) { + return undefined; + } + return { sendUin: NTRet.msgList[0].senderUin, sendTime: NTRet.msgList[0].msgTime } + } + let currentGroupMembers = groupMembers.get(GroupCode); + let PromiseData: Promise<({ + sendUin: string; + sendTime: string; + } | undefined)>[] = []; + let ret: Map = new Map(); + if (!currentGroupMembers) { + return ret; + } + for (let member of currentGroupMembers.values()) { + PromiseData.push(getdata(member.uid).catch(() => undefined)); + } + let allRet = await runAllWithTimeout(PromiseData, 2500); + for (let PromiseDo of allRet) { + if (PromiseDo) { + ret.set(PromiseDo.sendUin, PromiseDo.sendTime); + } + } + return ret; + } + static async getLastestMsgByUids(GroupCode: string, uids: string[]) { + let ret = await napCatCore.session.getMsgService().queryMsgsWithFilterEx('0', '0', '0', { + chatInfo: { + peerUid: GroupCode, + chatType: ChatType.group, + }, + filterMsgType: [], + filterSendersUid: uids, + filterMsgToTime: '0', + filterMsgFromTime: '0', + isReverseOrder: false, + isIncludeCurrent: true, + pageLimit: 1, + }); + return ret; + } + static async getGroupMemberAll(GroupCode: string, forced = false) { + return napCatCore.session.getGroupService().getAllMemberList(GroupCode, forced); + } + static async getLastestMsg(GroupCode: string, uins: string[]) { + let uids: Array = []; + for (let uin of uins) { + let uid = await NTQQUserApi.getUidByUin(uin) + if (uid) { + uids.push(uid); + } + } + let ret = await napCatCore.session.getMsgService().queryMsgsWithFilterEx('0', '0', '0', { + chatInfo: { + peerUid: GroupCode, + chatType: ChatType.group, + }, + filterMsgType: [], + filterSendersUid: uids, + filterMsgToTime: '0', + filterMsgFromTime: '0', + isReverseOrder: false, + isIncludeCurrent: true, + pageLimit: 1, + }); + return ret; + } + static async getGroupRecommendContactArkJson(GroupCode: string) { + return napCatCore.session.getGroupService().getGroupRecommendContactArkJson(GroupCode); + } + static async CreatGroupFileFolder(groupCode: string, folderName: string) { + return napCatCore.session.getRichMediaService().createGroupFolder(groupCode, folderName); + } + static async DelGroupFile(groupCode: string, files: string[]) { + return napCatCore.session.getRichMediaService().deleteGroupFile(groupCode, [102], files); + } + static async DelGroupFileFolder(groupCode: string, folderId: string) { + return napCatCore.session.getRichMediaService().deleteGroupFolder(groupCode, folderId); + } + static async addGroupEssence(GroupCode: string, msgId: string) { + // 代码没测过 + // 需要 ob11msgid->msgId + (peer) -> msgSeq + msgRandom + let MsgData = await napCatCore.session.getMsgService().getMsgsIncludeSelf({ chatType: 2, guildId: '', peerUid: GroupCode }, msgId, 1, false); + let param = { + groupCode: GroupCode, + msgRandom: parseInt(MsgData.msgList[0].msgRandom), + msgSeq: parseInt(MsgData.msgList[0].msgSeq) + }; + // GetMsgByShoretID(ShoretID); -> MsgService.getMsgs(Peer,MsgId,1,false); -> 组出参数 + return napCatCore.session.getGroupService().addGroupEssence(param); + } + static async removeGroupEssence(GroupCode: string, msgId: string) { + // 代码没测过 + // 需要 ob11msgid->msgId + (peer) -> msgSeq + msgRandom + let MsgData = await napCatCore.session.getMsgService().getMsgsIncludeSelf({ chatType: 2, guildId: '', peerUid: GroupCode }, msgId, 1, false); + let param = { + groupCode: GroupCode, + msgRandom: parseInt(MsgData.msgList[0].msgRandom), + msgSeq: parseInt(MsgData.msgList[0].msgSeq) + }; + // GetMsgByShoretID(ShoretID); -> MsgService.getMsgs(Peer,MsgId,1,false); -> 组出参数 + return napCatCore.session.getGroupService().removeGroupEssence(param); + } + static async getSingleScreenNotifies(num: number) { + let [_retData, _doubt, _seq, notifies] = await NTEventDispatch.CallNormalEvent + <(arg1: boolean, arg2: string, arg3: number) => Promise, (doubt: boolean, seq: string, notifies: GroupNotify[]) => void> + ( + 'NodeIKernelGroupService/getSingleScreenNotifies', + 'NodeIKernelGroupListener/onGroupSingleScreenNotifies', + 1, + 5000, + () => true, + false, + '', + num + ); + return notifies; + } + static async getGroupMemberV2(GroupCode: string, uid: string, forced = false) { + type ListenerType = NodeIKernelGroupListener['onMemberInfoChange']; + type EventType = NodeIKernelGroupService['getMemberInfo']; + // NTEventDispatch.CreatListenerFunction('NodeIKernelGroupListener/onGroupMemberInfoUpdate', + //return napCatCore.session.getGroupService().getMemberInfo(GroupCode, [uid], forced); + const [ret, _groupCode, _changeType, _members] = await NTEventDispatch.CallNormalEvent + + ( + 'NodeIKernelGroupService/getMemberInfo', + 'NodeIKernelGroupListener/onMemberInfoChange', + 1, + 5000, + (groupCode: string, changeType: number, members: Map) => { + return groupCode == GroupCode && members.has(uid); + }, + GroupCode, [uid], forced + ); + return _members.get(uid); + } + static async getGroupMembers(groupQQ: string, num = 3000): Promise> { + const groupService = napCatCore.session.getGroupService(); + const sceneId = groupService.createMemberListScene(groupQQ, 'groupMemberList_MainWindow'); + const result = await groupService.getNextMemberList(sceneId!, undefined, num); + if (result.errCode !== 0) { + throw ('获取群成员列表出错,' + result.errMsg); + } + + //logDebug(`获取群(${groupQQ})成员列表结果:`, `finish: ${result.result.finish}`); //, Array.from(result.result.infos.values())); + return result.result.infos; + /* + console.log(sceneId); + const result = await napCatCore.getGroupService().getNextMemberList(sceneId, num); + console.log(result); + + return result; + */ + } + + static async getGroupNotifies() { + // 获取管理员变更 + // 加群通知,退出通知,需要管理员权限 + + } + static async GetGroupFileCount(Gids: Array) { + return napCatCore.session.getRichMediaService().batchGetGroupFileCount(Gids); + } + static async getGroupIgnoreNotifies() { + } + static async getArkJsonGroupShare(GroupCode: string) { + let ret = await NTEventDispatch.CallNoListenerEvent + <(GroupId: string) => Promise>( + 'NodeIKernelGroupService/getGroupRecommendContactArkJson', + 5000, + GroupCode + ); + return ret.arkJson; + } + //需要异常处理 + static async uploadGroupBulletinPic(GroupCode: string, imageurl: string) { + const _Pskey = (await NTQQUserApi.getPSkey(['qun.qq.com'])).domainPskeyMap.get('qun.qq.com')!; + return napCatCore.session.getGroupService().uploadGroupBulletinPic(GroupCode, _Pskey, imageurl); + } + static async handleGroupRequest(flag: string, operateType: GroupRequestOperateTypes, reason?: string) { + let flagitem = flag.split('|'); + let groupCode = flagitem[0]; + let seq = flagitem[1]; + let type = parseInt(flagitem[2]); + + return napCatCore.session.getGroupService().operateSysNotify( + false, + { + 'operateType': operateType, // 2 拒绝 + 'targetMsg': { + 'seq': seq, // 通知序列号 + 'type': type, + 'groupCode': groupCode, + 'postscript': reason || ' ' // 仅传空值可能导致处理失败,故默认给个空格 + } + }); + } + + static async quitGroup(groupQQ: string) { + return napCatCore.session.getGroupService().quitGroup(groupQQ); + } + + static async kickMember(groupQQ: string, kickUids: string[], refuseForever: boolean = false, kickReason: string = '') { + return napCatCore.session.getGroupService().kickMember(groupQQ, kickUids, refuseForever, kickReason); + } + + static async banMember(groupQQ: string, memList: Array<{ uid: string, timeStamp: number }>) { + // timeStamp为秒数, 0为解除禁言 + return napCatCore.session.getGroupService().setMemberShutUp(groupQQ, memList); + } + + static async banGroup(groupQQ: string, shutUp: boolean) { + return napCatCore.session.getGroupService().setGroupShutUp(groupQQ, shutUp); + } + + static async setMemberCard(groupQQ: string, memberUid: string, cardName: string) { + return napCatCore.session.getGroupService().modifyMemberCardName(groupQQ, memberUid, cardName); + } + + static async setMemberRole(groupQQ: string, memberUid: string, role: GroupMemberRole) { + return napCatCore.session.getGroupService().modifyMemberRole(groupQQ, memberUid, role); + } + + static async setGroupName(groupQQ: string, groupName: string) { + return napCatCore.session.getGroupService().modifyGroupName(groupQQ, groupName, false); + } + + // 头衔不可用 + static async setGroupTitle(groupQQ: string, uid: string, title: string) { + + } + + static async publishGroupBulletin(groupQQ: string, content: string, picInfo: { id: string, width: number, height: number } | undefined = undefined, pinned: number = 0, confirmRequired: number = 0,) { + const _Pskey = (await NTQQUserApi.getPSkey(['qun.qq.com'])).domainPskeyMap.get('qun.qq.com'); + //text是content内容url编码 + let data = { + text: encodeURI(content), + picInfo: picInfo, + oldFeedsId: '', + pinned: pinned, + confirmRequired: confirmRequired + }; + return napCatCore.session.getGroupService().publishGroupBulletin(groupQQ, _Pskey!, data); + } + static async getGroupRemainAtTimes(GroupCode: string) { + napCatCore.session.getGroupService().getGroupRemainAtTimes(GroupCode); + } + static async getMemberExtInfo(groupCode: string, uin: string) { + // 仅NTQQ 9.9.11 24568测试 容易炸开谨慎使用 + return napCatCore.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 + } + } + ); + } +} \ No newline at end of file diff --git a/src/core/apis/index.ts b/src/core/apis/index.ts new file mode 100644 index 00000000..4c0619e3 --- /dev/null +++ b/src/core/apis/index.ts @@ -0,0 +1,8 @@ +export * from './file'; +export * from './friend'; +export * from './group'; +export * from './msg'; +export * from './user'; +export * from './webapi'; +export * from './sign'; +export * from './system'; \ No newline at end of file diff --git a/src/core/apis/msg.ts b/src/core/apis/msg.ts new file mode 100644 index 00000000..53c9ba88 --- /dev/null +++ b/src/core/apis/msg.ts @@ -0,0 +1,317 @@ +import { GetFileListParam, Peer, RawMessage, SendMessageElement, SendMsgElementConstructor } from '@/core/entities'; +import { friends, groups, selfInfo } from '@/core/data'; +import { log, logWarn } from '@/common/utils/log'; +import { sleep } from '@/common/utils/helper'; +import { napCatCore, NTQQUserApi } from '@/core'; +import { onGroupFileInfoUpdateParamType } from '@/core/listeners'; +import { GeneralCallResult } from '@/core/services/common'; +import { MessageUnique } from '../../../common/utils/MessageUnique'; +import { NTEventDispatch } from '@/common/utils/EventTask'; +import { requireMinNTQQBuild } from '@/common/utils/QQBasicInfo'; + +async function LoadMessageIdList(Peer: Peer, msgId: string) { + let msgList = await NTQQMsgApi.getMsgHistory(Peer, msgId, 50); + for (let j = 0; j < msgList.msgList.length; j++) { + let shortId = MessageUnique.createMsg(Peer, msgList.msgList[j].msgId); + } +} +async function loadMessageUnique() { + if (groups.size > 100) { + logWarn('[性能检测] 群数量大于100,可能会导致性能问题'); + } + let predict = (groups.size + friends.size / 2) / 5; + predict = predict < 20 ? 20 : predict; + predict = predict > 50 ? 50 : predict; + //let waitpromise: Array> = []; + predict = Math.floor(predict * 50); + MessageUnique.resize(predict); + let RecentContact = await NTQQUserApi.getRecentContactListSnapShot(predict); + let LoadMessageIdDo: Array> = new Array>(); + if (RecentContact?.info?.changedList && RecentContact?.info?.changedList?.length > 0) { + for (let i = 0; i < RecentContact.info.changedList.length; i++) { + let Peer: Peer = { chatType: RecentContact.info.changedList[i].chatType, peerUid: RecentContact.info.changedList[i].peerUid, guildId: '' }; + LoadMessageIdDo.push(LoadMessageIdList(Peer, RecentContact.info.changedList[i].msgId)); + } + } + await Promise.all(LoadMessageIdDo).then(() => { + log(`[消息序列] 加载 ${predict} 条历史消息记录完成`); + }); +} + +setTimeout(() => { + napCatCore.onLoginSuccess(async () => { + await sleep(100); + // NTQQMsgApi.CheckSendMode().then().catch(); + loadMessageUnique().then().catch(); + //下面的代码还没摸清 不要使用 + //let data = await napCatCore.session.getMsgService().sendSsoCmdReqByContend("LightAppSvc.mini_app_growguard.ReportExecute","1124343"); + //console.log(data); + }); +}, 100); +//歇菜LocalMsg压根不写Db +// setTimeout(async () => { +// let ele: MessageElement = { extBufForUI: '0x', ...SendMsgElementConstructor.text('测试消息') }; +// let MsgId = await NTQQMsgApi.getMsgUniqueEx(); +// let peer = { chatType: 2, peerUid: '', guildId: '' }; +// console.log(await napCatCore.session.getTestPerformanceService().insertMsg( +// { +// peer: peer, +// msgTime: Math.floor(Date.now() / 1000).toString(), +// msgId: MsgId, +// msgSeq: '56564', +// batchNums: 1, +// timesPerBatch: 1, +// numPerTime: 1 +// }, [ele] +// )); +// console.log(await NTQQMsgApi.multiForwardMsg(peer, peer, [MsgId])); +// }, 25000) + +export class NTQQMsgApi { + static async FetchLongMsg(peer: Peer, msgId: string) { + return napCatCore.session.getMsgService().fetchLongMsg(peer, msgId); + } + static async getMsgEmojiLikesList(peer: Peer, msgSeq: string, emojiId: string, emojiType: string, count: number = 20) { + //console.log(peer, msgSeq, emojiId, emojiType, count); + //注意此处emojiType 可选值一般为1-2 2好像是unicode表情dec值 大部分情况 Taged M likiowa + return napCatCore.session.getMsgService().getMsgEmojiLikesList(peer, msgSeq, emojiId, emojiType, "", false, 20) + } + // static napCatCore: NapCatCore | null = null; + // enum BaseEmojiType { + // NORMAL_EMOJI, + // SUPER_EMOJI, + // RANDOM_SUPER_EMOJI, + // CHAIN_SUPER_EMOJI, + // EMOJI_EMOJI + // } + static async setEmojiLike(peer: Peer, msgSeq: string, emojiId: string, set: boolean = true) { + // nt_qq//global//nt_data//Emoji//emoji-resource//sysface_res/apng/ 下可以看到所有QQ表情预览 + // nt_qq\global\nt_data\Emoji\emoji-resource\face_config.json 里面有所有表情的id, 自带表情id是QSid, 标准emoji表情id是QCid + // 其实以官方文档为准是最好的,https://bot.q.qq.com/wiki/develop/api-v2/openapi/emoji/model.html#EmojiType + emojiId = emojiId.toString(); + return napCatCore.session.getMsgService().setMsgEmojiLikes(peer, msgSeq, emojiId, emojiId.length > 3 ? '2' : '1', set); + } + static async getMultiMsg(peer: Peer, rootMsgId: string, parentMsgId: string): Promise { + return napCatCore.session.getMsgService().getMultiMsg(peer, rootMsgId, parentMsgId); + } + static async ForwardMsg(peer: Peer, msgIds: string[]) { + return napCatCore.session.getMsgService().forwardMsg(msgIds, peer, [peer], new Map()); + } + static async getLastestMsgByUids(peer: Peer, count: number = 20, isReverseOrder: boolean = false) { + let ret = await napCatCore.session.getMsgService().queryMsgsWithFilterEx('0', '0', '0', { + chatInfo: peer, + filterMsgType: [], + filterSendersUid: [], + filterMsgToTime: '0', + filterMsgFromTime: '0', + isReverseOrder: isReverseOrder,//此参数有点离谱 注意不是本次查询的排序 而是全部消历史信息的排序 默认false 从新消息拉取到旧消息 + isIncludeCurrent: true, + pageLimit: count, + }); + return ret; + } + static async getMsgsByMsgId(peer: Peer | undefined, msgIds: string[] | undefined) { + if (!peer) throw new Error('peer is not allowed'); + if (!msgIds) throw new Error('msgIds is not allowed'); + //Mlikiowa: 参数不合规会导致NC异常崩溃 原因是TX未对进入参数判断 对应Android标记@NotNull AndroidJADX分析可得 + return await napCatCore.session.getMsgService().getMsgsByMsgId(peer, msgIds); + } + static async getSingleMsg(peer: Peer, seq: string) { + return await napCatCore.session.getMsgService().getSingleMsg(peer, seq); + } + static async fetchFavEmojiList(num: number) { + return napCatCore.session.getMsgService().fetchFavEmojiList("", num, true, true) + } + static async queryMsgsWithFilterExWithSeq(peer: Peer, msgSeq: string) { + let ret = await napCatCore.session.getMsgService().queryMsgsWithFilterEx('0', '0', msgSeq, { + chatInfo: peer,//此处为Peer 为关键查询参数 没有啥也没有 by mlik iowa + filterMsgType: [], + filterSendersUid: [], + filterMsgToTime: '0', + filterMsgFromTime: '0', + isReverseOrder: false, + isIncludeCurrent: true, + pageLimit: 1, + }); + return ret; + } + static async getMsgsBySeqAndCount(peer: Peer, seq: string, count: number, desc: boolean, z: boolean) { + return await napCatCore.session.getMsgService().getMsgsBySeqAndCount(peer, seq, count, desc, z); + } + static async setMsgRead(peer: Peer) { + return napCatCore.session.getMsgService().setMsgRead(peer); + } + static async getGroupFileList(GroupCode: string, params: GetFileListParam) { + let data = await NTEventDispatch.CallNormalEvent< + (GroupCode: string, params: GetFileListParam) => Promise, + (groupFileListResult: onGroupFileInfoUpdateParamType) => void + >( + 'NodeIKernelRichMediaService/getGroupFileList', + 'NodeIKernelMsgListener/onGroupFileInfoUpdate', + 1, + 5000, + (groupFileListResult: onGroupFileInfoUpdateParamType) => { + //Developer Mlikiowa Todo: 此处有问题 无法判断是否成功 + return true; + }, + GroupCode, + params + ); + return data[1].item; + } + static async getMsgHistory(peer: Peer, msgId: string, count: number, isReverseOrder: boolean = false) { + // 消息时间从旧到新 + return napCatCore.session.getMsgService().getMsgsIncludeSelf(peer, msgId, count, isReverseOrder); + } + static async recallMsg(peer: Peer, msgIds: string[]) { + await napCatCore.session.getMsgService().recallMsg({ + chatType: peer.chatType, + peerUid: peer.peerUid + }, msgIds); + } + static async sendMsgV2(peer: Peer, msgElements: SendMessageElement[], waitComplete = true, timeout = 10000) { + function generateMsgId() { + const timestamp = Math.floor(Date.now() / 1000); + const random = Math.floor(Math.random() * Math.pow(2, 32)); + const buffer = Buffer.alloc(8); + buffer.writeUInt32BE(timestamp, 0); + buffer.writeUInt32BE(random, 4); + const msgId = BigInt("0x" + buffer.toString('hex')).toString(); + return msgId; + } + // 此处有采用Hack方法 利用数据返回正确得到对应消息 + // 与之前 Peer队列 MsgSeq队列 真正的MsgId并发不同 + // 谨慎采用 目前测试暂无问题 Developer.Mlikiowa + let msgId: string; + try { + msgId = await NTQQMsgApi.getMsgUnique(peer.chatType, await NTQQMsgApi.getServerTime()); + } catch (error) { + //if (!napCatCore.session.getMsgService()['generateMsgUniqueId']) + //兜底识别策略V2 + msgId = generateMsgId().toString(); + } + let data = await NTEventDispatch.CallNormalEvent< + (msgId: string, peer: Peer, msgElements: SendMessageElement[], map: Map) => Promise, + (msgList: RawMessage[]) => void + >( + 'NodeIKernelMsgService/sendMsg', + 'NodeIKernelMsgListener/onMsgInfoListUpdate', + 1, + timeout, + (msgRecords: RawMessage[]) => { + for (let msgRecord of msgRecords) { + if (msgRecord.msgId === msgId && msgRecord.sendStatus === 2) { + return true; + } + } + return false; + }, + msgId, + peer, + msgElements, + new Map() + ); + let retMsg = data[1].find(msgRecord => { + if (msgRecord.msgId === msgId) { + return true; + } + }); + return retMsg; + } + static sendMsgEx(peer: Peer, msgElements: SendMessageElement[], waitComplete = true, timeout = 10000) { + //return NTQQMsgApi.sendMsgV1(peer, msgElements, waitComplete, timeout); + } + static async sendMsg(peer: Peer, msgElements: SendMessageElement[], waitComplete = true, timeout = 10000) { + //唉? !我有个想法 + let msgId = await NTQQMsgApi.getMsgUnique(peer.chatType, await NTQQMsgApi.getServerTime()); + peer.guildId = msgId; + let data = await NTEventDispatch.CallNormalEvent< + (msgId: string, peer: Peer, msgElements: SendMessageElement[], map: Map) => Promise, + (msgList: RawMessage[]) => void + >( + 'NodeIKernelMsgService/sendMsg', + 'NodeIKernelMsgListener/onMsgInfoListUpdate', + 1, + timeout, + (msgRecords: RawMessage[]) => { + for (let msgRecord of msgRecords) { + if (msgRecord.guildId === msgId && msgRecord.sendStatus === 2) { + return true; + } + } + return false; + }, + "0", + peer, + msgElements, + new Map() + ); + let retMsg = data[1].find(msgRecord => { + if (msgRecord.guildId === msgId) { + return true; + } + }); + return retMsg; + } + static async getMsgUnique(chatType: number, time: string) { + if (requireMinNTQQBuild('26702')) { + return napCatCore.session.getMsgService().generateMsgUniqueId(chatType, time); + } + return napCatCore.session.getMsgService().getMsgUniqueId(time); + } + static async getServerTime() { + return napCatCore.session.getMSFService().getServerTime(); + } + static async getServerTimeV2() { + return NTEventDispatch.CallNoListenerEvent<() => string>('NodeIKernelMsgService/getServerTime', 5000); + } + static async forwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) { + return napCatCore.session.getMsgService().forwardMsg(msgIds, srcPeer, [destPeer], new Map()); + } + static async multiForwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]): Promise { + const msgInfos = msgIds.map(id => { + return { msgId: id, senderShowName: selfInfo.nick }; + }); + let data = await NTEventDispatch.CallNormalEvent< + (msgInfo: typeof msgInfos, srcPeer: Peer, destPeer: Peer, comment: Array, attr: Map,) => Promise, + (msgList: RawMessage[]) => void + >( + 'NodeIKernelMsgService/multiForwardMsgWithComment', + 'NodeIKernelMsgListener/onMsgInfoListUpdate', + 1, + 5000, + (msgRecords: RawMessage[]) => { + for (let msgRecord of msgRecords) { + if (msgRecord.peerUid == destPeer.peerUid && msgRecord.senderUid == selfInfo.uid) { + return true; + } + } + return false; + }, + msgInfos, + srcPeer, + destPeer, + [], + new Map() + ); + for (let msg of data[1]) { + const arkElement = msg.elements.find(ele => ele.arkElement); + if (!arkElement) { + continue; + } + const forwardData: any = JSON.parse(arkElement.arkElement.bytesData); + if (forwardData.app != 'com.tencent.multimsg') { + continue; + } + if (msg.peerUid == destPeer.peerUid && msg.senderUid == selfInfo.uid) { + return msg; + } + } + throw new Error('转发消息超时'); + } + static async markallMsgAsRead() { + return napCatCore.session.getMsgService().setAllC2CAndGroupMsgRead(); + } +} diff --git a/src/core/apis/sign.ts b/src/core/apis/sign.ts new file mode 100644 index 00000000..8da3ab8e --- /dev/null +++ b/src/core/apis/sign.ts @@ -0,0 +1,308 @@ +import { logDebug } from '@/common/utils/log'; +import { NTQQUserApi } from './user'; +import { selfInfo } from '../data'; +import { RequestUtil } from '@/common/utils/request'; +import { WebApi } from './webapi'; +import { checkFileReceived, checkFileReceived2, uri2local } from '@/common/utils/file'; +import fs from 'node:fs' +import { sleep } from '@/common/utils/helper'; +export interface IdMusicSignPostData { + type: 'qq' | '163', + id: string | number, +} + +export interface CustomMusicSignPostData { + type: 'custom', + url: string, + audio: string, + title: string, + image?: string, + singer?: string +} +// let t = await napCatCore.session.getGroupService().shareDigest({ +// appId: "100497308", +// appType: 1, +// msgStyle: 0, +// recvUin: "726067488", +// sendType: 1, +// clientInfo: { +// platform: 1 +// }, +// richMsg: { +// usingArk: true, +// title: "Bot测试title", +// summary: "Bot测试summary", +// url: "https://www.bilibili.com", +// pictureUrl: "https://y.qq.com/music/photo_new/T002R300x300M0000035DC6W4ZpSqf_1.jpg?max_age=2592000", +// brief: "Bot测试brief", +// } +// }); +// { +// errCode: 0, +// errMsg: '', +// rsp: { +// sendType: 1, +// recvUin: '726067488', +// recvOpenId: '', +// errCode: 901501, +// errMsg: 'imagent service_error:150_OIDB_NO_PRIV', +// extInfo: { +// wording: '消息下发失败(错误码:901501)', +// jumpResult: 0, +// jumpUrl: '', +// level: 0, +// subLevel: 0, +// developMsg: 'imagent error' +// } +// } +// } + +// export class MusicSign { +// private readonly url: string; + +// constructor(url: string) { +// this.url = url; +// } + +// sign(postData: CustomMusicSignPostData | IdMusicSignPostData): Promise { +// return new Promise((resolve, reject) => { +// fetch(this.url, { +// method: 'POST', // 指定请求方法为 POST +// headers: { +// 'Content-Type': 'application/json' // 设置请求头,指明发送的数据类型为 JSON +// }, +// body: JSON.stringify(postData) // 将 JavaScript 对象转换为 JSON 字符串作为请求体 +// }) +// .then(response => { +// if (!response.ok) { +// reject(response.statusText); // 请求失败,返回错误信息 +// } +// return response.json(); // 解析 JSON 格式的响应体 +// }) +// .then(data => { +// logDebug('音乐消息生成成功', data); +// resolve(data); +// }) +// .catch(error => { +// reject(error); +// }); +// }); +// } +// } +export interface MiniAppLuaJsonType { + prompt: string, + title: string, + preview: string, + jumpUrl: string, + tag: string, + tagIcon: string, + source: string, + sourcelogo: string, +} +export async function SignMiniApp(CardData: MiniAppLuaJsonType) { + // { + // "app": "com.tencent.miniapp.lua", + // "bizsrc": "tianxuan.imgJumpArk", + // "view": "miniapp", + // "prompt": "hi! 这里有我的日常故事,只想讲给你听", + // "config": { + // "type": "normal", + // "forward": 1, + // "autosize": 0 + // }, + // "meta": { + // "miniapp": { + // "title": "hi! 这里有我的日常故事,只想讲给你听", + // "preview": "https:\/\/tianquan.gtimg.cn\/qqAIAgent\/item\/7\/square.png", + // "jumpUrl": "https:\/\/club.vip.qq.com\/transfer?open_kuikly_info=%7B%22version%22%3A%20%221%22%2C%22src_type%22%3A%20%22web%22%2C%22kr_turbo_display%22%3A%20%221%22%2C%22page_name%22%3A%20%22vas_ai_persona_moments%22%2C%22bundle_name%22%3A%20%22vas_ai_persona_moments%22%7D&page_name=vas_ai_persona_moments&enteranceId=share&robot_uin=3889008584", + // "tag": "QQ智能体", + // "tagIcon": "https:\/\/tianquan.gtimg.cn\/shoal\/qqAIAgent\/3e9d70c9-d98c-45b8-80b4-79d82971b514.png", + // "source": "QQ智能体", + // "sourcelogo": "https:\/\/tianquan.gtimg.cn\/shoal\/qqAIAgent\/3e9d70c9-d98c-45b8-80b4-79d82971b514.png" + // } + // } + // } + + // token : function(url,skey){ + // var str = skey || cookie('skey') || cookie('rv2') || '', + // hash = 5381; + // if(url){ + // var hostname = uri(url).hostname; + // if(hostname.indexOf('qun.qq.com') > -1 || (hostname.indexOf('qzone.qq.com') > -1 && hostname.indexOf('qun.qzone.qq.com') === -1)){ + // str = cookie('p_skey') || str; + // } + // } + // for(var i = 0, len = str.length; i < len; ++i){ + // hash += (hash << 5) + str.charAt(i).charCodeAt(); + // } + // return hash & 0x7fffffff; + // }, + // + + // function signToken(skey: string) { + // let hash = 5381; + // for (let i = 0, len = skey.length; i < len; ++i) { + // hash += (hash << 5) + skey.charCodeAt(i); + // } + // return hash & 0x7fffffff; + // } + let signCard = { + "app": "com.tencent.miniapp.lua", + "bizsrc": "tianxuan.imgJumpArk", + "view": "miniapp", + "prompt": CardData.prompt, + "config": { + "type": "normal", + "forward": 1, + "autosize": 0 + }, + "meta": { + "miniapp": { + "title": CardData.title, + "preview": (CardData.preview as string).replace(/\\/g, "\\/\\/"), + "jumpUrl": (CardData.jumpUrl as string).replace(/\\/g, "\\/\\/"), + "tag": CardData.tag, + "tagIcon": (CardData.tagIcon as string).replace(/\\/g, "\\/\\/"), + "source": CardData.source, + "sourcelogo": (CardData.sourcelogo as string).replace(/\\/g, "\\/\\/") + } + } + }; + // let signCard = { + // "app": "com.tencent.eventshare.lua", + // "prompt": "Bot Test", + // "bizsrc": "tianxuan.business", + // "meta": { + // "eventshare": { + // "button1URL": "https://www.bilibili.com", + // "button1disable": false, + // "button1title": "点我前往", + // "button2URL": "", + // "button2disable": false, + // "button2title": "", + // "buttonNum": 1, + // "jumpURL": "https://www.bilibili.com", + // "preview": "https://tianquan.gtimg.cn/shoal/card/9930bc4e-4a92-4da3-814f-8094a2421d9c.png", + // "tag": "QQ集卡", + // "tagIcon": "https://tianquan.gtimg.cn/shoal/card/c034854b-102d-40be-a545-5ca90a7c49c9.png", + // "title": "Bot Test" + // } + // }, + // "config": { + // "autosize": 0, + // "collect": 0, + // "ctime": 1716568575, + // "forward": 1, + // "height": 336, + // "reply": 0, + // "round": 1, + // "type": "normal", + // "width": 263 + // }, + // "view": "eventshare", + // "ver": "0.0.0.1" + // }; + let data = (await NTQQUserApi.getQzoneCookies()); + const Bkn = WebApi.genBkn(data.p_skey); + + const CookieValue = 'p_skey=' + data.p_skey + '; skey=' + data.skey + '; p_uin=o' + selfInfo.uin + '; uin=o' + selfInfo.uin; + + let signurl = "https://h5.qzone.qq.com/v2/vip/tx/trpc/ark-share/GenNewSignedArk?g_tk=" + Bkn + "&ark=" + encodeURIComponent(JSON.stringify(signCard)); + let signed_ark = ""; + try { + let retData = await RequestUtil.HttpGetJson<{ code: number, data: { signed_ark: string } }>(signurl, 'GET', undefined, { Cookie: CookieValue }); + //logDebug('MiniApp JSON 消息生成成功', retData); + signed_ark = retData.data.signed_ark; + } catch (error) { + logDebug('MiniApp JSON 消息生成失败', error); + } + return signed_ark; +} +export async function SignMusicInternal(songname: string, singer: string, cover: string, songmid: string, songmusic: string) { + //curl -X POST 'https://mqq.reader.qq.com/api/mqq/share/card?accessToken&_csrfToken&source=c0003' -H 'Content-Type: application/json' -H 'Cookie: uin=o10086' -d '{"app":"com.tencent.qqreader.share","config":{"ctime":1718634110,"forward":1,"token":"9a63343c32d5a16bcde653eb97faa25d","type":"normal"},"extra":{"app_type":1,"appid":100497308,"msg_seq":14386738075403815000.0,"uin":1733139081},"meta":{"music":{"action":"","android_pkg_name":"","app_type":1,"appid":100497308,"ctime":1718634110,"desc":"周杰伦","jumpUrl":"https://i.y.qq.com/v8/playsong.html?songmid=0039MnYb0qxYhV&type=0","musicUrl":"http://ws.stream.qqmusic.qq.com/http://isure6.stream.qqmusic.qq.com/M800002202B43Cq4V4.mp3?fromtag=810033622&guid=br_xzg&trace=23fe7bcbe2336bbf&uin=553&vkey=CF0F5CE8B0FA16F3001F8A88D877A217EB5E4F00BDCEF1021EB6C48969CA33C6303987AEECE9CC840122DD2F917A59D6130D8A8CA4577C87","preview":"https://y.qq.com/music/photo_new/T002R800x800M000000MkMni19ClKG.jpg","cover":"https://y.qq.com/music/photo_new/T002R800x800M000000MkMni19ClKG.jpg","sourceMsgId":"0","source_icon":"https://p.qpic.cn/qqconnect/0/app_100497308_1626060999/100?max-age=2592000&t=0","source_url":"","tag":"QQ音乐","title":"晴天","uin":10086}},"prompt":"[分享]晴天","ver":"0.0.0.1","view":"music"}' + let signurl = 'https://mqq.reader.qq.com/api/mqq/share/card?accessToken&_csrfToken&source=c0003'; + //let = "https://y.qq.com/music/photo_new/T002R800x800M000000MkMni19ClKG.jpg"; + let signCard = { + app: "com.tencent.qqreader.share", + config: { + ctime: 1718634110, + forward: 1, + token: "9a63343c32d5a16bcde653eb97faa25d", + type: "normal" + }, + extra: { + app_type: 1, + appid: 100497308, + msg_seq: 14386738075403815000.0, + uin: 1733139081 + }, + meta: { + music: + { + action: "", + android_pkg_name: "", + app_type: 1, + appid: 100497308, + ctime: 1718634110, + desc: singer, + jumpUrl: "https://i.y.qq.com/v8/playsong.html?songmid=" + songmid + "&type=0", + musicUrl: songmusic, + preview: cover, + cover: cover, + sourceMsgId: "0", + source_icon: "https://p.qpic.cn/qqconnect/0/app_100497308_1626060999/100?max-age=2592000&t=0", + source_url: "", + tag: "QQ音乐", + title: songname, + uin: 10086 + } + }, + prompt: "[分享]" + songname, + ver: "0.0.0.1", + view: "music" + } + //console.log(JSON.stringify(signCard, null, 2)); + let data = await RequestUtil.HttpGetJson<{ code: number, data: { arkResult: string } }> + (signurl, 'POST', signCard, { 'Cookie': 'uin=o10086', 'Content-Type': 'application/json' }); + return data; +} +//注意处理错误 +export async function CreateMusicThridWay0(id: string = '', mid: string = '') { + if (mid == '') { + let MusicInfo = await RequestUtil.HttpGetJson + <{ songinfo?: { data?: { track_info: { mid: string } } } }> + ( + 'https://u.y.qq.com/cgi-bin/musicu.fcg?format=json&inCharset=utf8&outCharset=utf-8¬ice=0&platform=yqq.json&needNewCode=0&data={"comm":{"ct":24,"cv":0},"songinfo":{"method":"get_song_detail_yqq","param":{"song_type":0,"song_mid":"","song_id":' + id + '},"module":"music.pf_song_detail_svr"}}', + 'GET', + undefined + ); + mid = MusicInfo.songinfo?.data?.track_info.mid!; + } + //第三方接口 存在速率限制 现在勉强用 + let MusicReal = await RequestUtil.HttpGetJson + <{ code: number, data?: { name: string, singer: string, url: string, cover: string } }> + ('https://api.leafone.cn/api/qqmusic?id=' + mid + '&type=8', 'GET', undefined); + //console.log(MusicReal); + return { ...MusicReal.data, mid: mid }; +} +export async function CreateMusicThridWay1(id: string = '', mid: string = '') { + +} +//转换外域名为 https://qq.ugcimg.cn/v1/cpqcbu4b8870i61bde6k7cbmjgejq8mr3in82qir4qi7ielffv5slv8ck8g42novtmev26i233ujtuab6tvu2l2sjgtupfr389191v00s1j5oh5325j5eqi40774jv1i/khovifoh7jrqd6eahoiv7koh8o +//https://cgi.connect.qq.com/qqconnectopen/openapi/change_image_url?url=https://th.bing.com/th?id=OSK.b8ed36f1fb1889de6dc84fd81c187773&w=46&h=46&c=11&rs=1&qlt=80&o=6&dpr=2&pid=SANGAM + +//外域名不行得走qgroup中转 +//https://proxy.gtimg.cn/tx_tls_gate=y.qq.com/music/photo_new/T002R800x800M000000y5gq7449K9I.jpg + +//可外域名 +//https://pic.ugcimg.cn/500955bdd6657ecc8e82e02d2df06800/jpg1 + +//QQ音乐gtimg接口 +//https://y.gtimg.cn/music/photo_new/T002R800x800M000000y5gq7449K9I.jpg?max_age=2592000 + +//还有一处公告上传可以上传高质量图片 持久为qq域名 +export async function SignMusicWrapper(id: string = '') { + let MusicInfo = await CreateMusicThridWay0(id)!; + let MusicCard = await SignMusicInternal(MusicInfo.name!, MusicInfo.singer!, MusicInfo.cover!, MusicInfo.mid!, "https://ws.stream.qqmusic.qq.com/" + MusicInfo.url!); + return MusicCard; +} \ No newline at end of file diff --git a/src/core/apis/system.ts b/src/core/apis/system.ts new file mode 100644 index 00000000..52d3f997 --- /dev/null +++ b/src/core/apis/system.ts @@ -0,0 +1,35 @@ + + +import { NTEventDispatch } from '@/common/utils/EventTask'; +import { GeneralCallResult, NTQQFileApi, NTQQUserApi, napCatCore } from '@/core'; +export class NTQQSystemApi { + static async hasOtherRunningQQProcess() { + return napCatCore.util.hasOtherRunningQQProcess(); + } + static async ORCImage(filePath: string) { + return napCatCore.session.getNodeMiscService().wantWinScreenOCR(filePath); + } + static async translateEnWordToZn(words: string[]) { + return napCatCore.session.getRichMediaService().translateEnWordToZn(words); + } + //调用会超时 没灯用 (好像是通知listener的) onLineDev + static async getOnlineDev() { + return napCatCore.session.getMsgService().getOnLineDev(); + } + //1-2-162b9b42-65b9-4405-a8ed-2e256ec8aa50 + static async getArkJsonCollection(cid: string) { + let ret = await NTEventDispatch.CallNoListenerEvent + <(cid: string) => Promise>( + 'NodeIKernelCollectionService/collectionArkShare', + 5000, + '1717662698058' + ); + return ret; + } + static async BootMiniApp(appfile: string, params: string) { + await napCatCore.session.getNodeMiscService().setMiniAppVersion('2.16.4'); + let c = await napCatCore.session.getNodeMiscService().getMiniAppPath(); + + return napCatCore.session.getNodeMiscService().startNewMiniApp(appfile, params); + } +} \ No newline at end of file diff --git a/src/core/apis/user.ts b/src/core/apis/user.ts new file mode 100644 index 00000000..dea9937f --- /dev/null +++ b/src/core/apis/user.ts @@ -0,0 +1,357 @@ +import { ModifyProfileParams, SelfInfo, User, UserDetailInfoByUin, UserDetailInfoByUinV2 } from '@/core/entities'; +import { friends, groupMembers, selfInfo } from '@/core/data'; +import { CacheClassFuncAsync, CacheClassFuncAsyncExtend } from '@/common/utils/helper'; +import { napCatCore, NTQQFriendApi } from '@/core'; +import { NodeIKernelProfileListener, ProfileListener } from '@/core/listeners'; +import { RequestUtil } from '@/common/utils/request'; +import { logWarn } from '@/common/utils/log'; +import { NTEventDispatch } from '@/common/utils/EventTask'; +import { NodeIKernelProfileService, ProfileBizType, UserDetailSource } from '@/core/services'; +import { requireMinNTQQBuild } from '@/common/utils/QQBasicInfo'; + +export class NTQQUserApi { + async getProfileLike(uid: string) { + return napCatCore.session.getProfileLikeService().getBuddyProfileLike({ + friendUids: [ + uid + ], + basic: 1, + vote: 1, + favorite: 0, + userProfile: 1, + type: 2, + start: 0, + limit: 20 + }); + } + async setLongNick(longNick: string) { + return napCatCore.session.getProfileService().setLongNick(longNick); + } + async setSelfOnlineStatus(status: number, extStatus: number, batteryStatus: number) { + return napCatCore.session.getMsgService().setStatus({ status: status, extStatus: extStatus, batteryStatus: batteryStatus }); + } + async getBuddyRecommendContactArkJson(uin: string, sencenID = '') { + return napCatCore.session.getBuddyService().getBuddyRecommendContactArkJson(uin, sencenID); + } + async like(uid: string, count = 1): Promise<{ result: number, errMsg: string, succCounts: number }> { + return napCatCore.session.getProfileLikeService().setBuddyProfileLike({ + friendUid: uid, + sourceId: 71, + doLikeCount: count, + doLikeTollCount: 0 + }); + } + + async setQQAvatar(filePath: string) { + type setQQAvatarRet = { result: number, errMsg: string }; + const ret = await napCatCore.session.getProfileService().setHeader(filePath) as setQQAvatarRet; + return { result: ret?.result, errMsg: ret?.errMsg }; + } + async setGroupAvatar(gc: string, filePath: string) { + return napCatCore.session.getGroupService().setHeader(gc, filePath); + } + + async fetchUserDetailInfos(uids: string[]) { + //26702 以上使用新接口 .Dev Mlikiowa + type EventService = NodeIKernelProfileService['fetchUserDetailInfo']; + type EventListener = NodeIKernelProfileListener['onUserDetailInfoChanged']; + let retData: User[] = []; + let [_retData, _retListener] = await NTEventDispatch.CallNormalEvent + + ( + 'NodeIKernelProfileService/fetchUserDetailInfo', + 'NodeIKernelProfileListener/onUserDetailInfoChanged', + uids.length, + 5000, + (profile) => { + if (uids.includes(profile.uid)) { + let RetUser: User = { + ...profile.simpleInfo.coreInfo, + ...profile.simpleInfo.status, + ...profile.simpleInfo.vasInfo, + ...profile.commonExt, + ...profile.simpleInfo.baseInfo, + qqLevel: profile.commonExt.qqLevel, + pendantId: "" + }; + retData.push(RetUser); + return true; + } + return false; + }, + "BuddyProfileStore", + uids, + UserDetailSource.KSERVER, + [ + ProfileBizType.KALL + ] + ); + + return retData; + } + async fetchUserDetailInfo(uid: string) { + type EventService = NodeIKernelProfileService['fetchUserDetailInfo']; + type EventListener = NodeIKernelProfileListener['onUserDetailInfoChanged']; + let [_retData, profile] = await NTEventDispatch.CallNormalEvent + + ( + 'NodeIKernelProfileService/fetchUserDetailInfo', + 'NodeIKernelProfileListener/onUserDetailInfoChanged', + 1, + 5000, + (profile) => { + if (profile.uid === uid) { + return true; + } + return false; + }, + "BuddyProfileStore", + [ + uid + ], + UserDetailSource.KSERVER, + [ + ProfileBizType.KALL + ] + ); + let RetUser: User = { + ...profile.simpleInfo.coreInfo, + ...profile.simpleInfo.status, + ...profile.simpleInfo.vasInfo, + ...profile.commonExt, + ...profile.simpleInfo.baseInfo, + qqLevel: profile.commonExt.qqLevel, + pendantId: "" + }; + return RetUser; + } + async getUserDetailInfo(uid: string) { + if (requireMinNTQQBuild('26702')) { + return this.fetchUserDetailInfo(uid); + } + return this.getUserDetailInfoOld(uid); + } + async getUserDetailInfoOld(uid: string) { + type EventService = NodeIKernelProfileService['getUserDetailInfoWithBizInfo']; + type EventListener = NodeIKernelProfileListener['onProfileDetailInfoChanged']; + let [_retData, profile] = await NTEventDispatch.CallNormalEvent + + ( + 'NodeIKernelProfileService/getUserDetailInfoWithBizInfo', + 'NodeIKernelProfileListener/onProfileDetailInfoChanged', + 2, + 5000, + (profile: User) => { + if (profile.uid === uid) { + return true; + } + return false; + }, + uid, + [0] + ); + return profile; + } + async modifySelfProfile(param: ModifyProfileParams) { + return napCatCore.session.getProfileService().modifyDesktopMiniProfile(param); + } + //需要异常处理 + @CacheClassFuncAsync(1800 * 1000) + async getCookies(domain: string) { + const ClientKeyData = await NTQQUserApi.forceFetchClientKey(); + const requestUrl = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + selfInfo.uin + '&clientkey=' + ClientKeyData.clientKey + '&u1=https%3A%2F%2F' + domain + '%2F' + selfInfo.uin + '%2Finfocenter&keyindex=19%27' + let cookies: { [key: string]: string; } = await RequestUtil.HttpsGetCookies(requestUrl); + return cookies; + } + @CacheClassFuncAsync(1800 * 1000) + async getPSkey(domainList: string[]) { + return await napCatCore.session.getTipOffService().getPskey(domainList, true); + } + async getRobotUinRange(): Promise> { + const robotUinRanges = await napCatCore.session.getRobotService().getRobotUinRange({ + justFetchMsgConfig: '1', + type: 1, + version: 0, + aioKeywordVersion: 0 + }); + // console.log(robotUinRanges?.response?.robotUinRanges); + return robotUinRanges?.response?.robotUinRanges; + } + //需要异常处理 + @CacheClassFuncAsync(1800 * 1000) + async getQzoneCookies() { + const ClientKeyData = await NTQQUserApi.forceFetchClientKey(); + const requestUrl = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + selfInfo.uin + '&clientkey=' + ClientKeyData.clientKey + '&u1=https%3A%2F%2Fuser.qzone.qq.com%2F' + selfInfo.uin + '%2Finfocenter&keyindex=19%27' + let cookies: { [key: string]: string; } = await RequestUtil.HttpsGetCookies(requestUrl); + return cookies; + } + //需要异常处理 + @CacheClassFuncAsync(1800 * 1000) + async getSkey(): Promise { + const ClientKeyData = await NTQQUserApi.forceFetchClientKey(); + if (ClientKeyData.result !== 0) { + throw new Error('getClientKey Error'); + } + const clientKey = ClientKeyData.clientKey; + const keyIndex = ClientKeyData.keyIndex; + const requestUrl = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + selfInfo.uin + '&clientkey=' + clientKey + '&u1=https%3A%2F%2Fh5.qzone.qq.com%2Fqqnt%2Fqzoneinpcqq%2Ffriend%3Frefresh%3D0%26clientuin%3D0%26darkMode%3D0&keyindex=19%27'; + let cookies: { [key: string]: string; } = await RequestUtil.HttpsGetCookies(requestUrl); + const skey = cookies['skey']; + if (!skey) { + throw new Error('getSkey Skey is Empty'); + } + return skey; + } + @CacheClassFuncAsyncExtend(3600 * 1000, 'Uin2Uid', (Uin: string, Uid: string | undefined) => { + if (Uid && Uid.indexOf('u_') != -1) { + return true + } + logWarn("uin转换到uid时异常", Uin, Uid); + return false; + }) + async getUidByUin(Uin: string) { + //此代码仅临时使用,后期会被废弃 + if (requireMinNTQQBuild('26702')) { + return await NTQQUserApi.getUidByUinV2(Uin); + } + return await NTQQUserApi.getUidByUinV1(Uin); + } + @CacheClassFuncAsyncExtend(3600 * 1000, 'Uid2Uin', (Uid: string | undefined, Uin: number | undefined) => { + if (Uin && Uin != 0 && !isNaN(Uin)) { + return true + } + logWarn("uid转换到uin时异常", Uid, Uin); + return false; + }) + async getUinByUid(Uid: string) { + //此代码仅临时使用,后期会被废弃 + if (requireMinNTQQBuild('26702')) { + return await NTQQUserApi.getUinByUidV2(Uid); + } + return await NTQQUserApi.getUinByUidV1(Uid); + } + + //后期改成流水线处理 + async getUidByUinV2(Uin: string) { + let uid = (await napCatCore.session.getProfileService().getUidByUin('FriendsServiceImpl', [Uin])).get(Uin); + if (uid) return uid; + uid = (await napCatCore.session.getGroupService().getUidByUins([Uin])).uids.get(Uin); + if (uid) return uid; + uid = (await napCatCore.session.getUixConvertService().getUid([Uin])).uidInfo.get(Uin); + if (uid) return uid; + console.log((await NTQQFriendApi.getBuddyIdMapCache(true))); + uid = (await NTQQFriendApi.getBuddyIdMapCache(true)).getValue(Uin);//从Buddy缓存获取Uid + if (uid) return uid; + uid = (await NTQQFriendApi.getBuddyIdMap(true)).getValue(Uin); + if (uid) return uid; + let unveifyUid = (await NTQQUserApi.getUserDetailInfoByUinV2(Uin)).detail.uid;//从QQ Native 特殊转换 + if (unveifyUid.indexOf("*") == -1) uid = unveifyUid; + //if (uid) return uid; + return uid; + } + //后期改成流水线处理 + async getUinByUidV2(Uid: string) { + let uin = (await napCatCore.session.getProfileService().getUinByUid('FriendsServiceImpl', [Uid])).get(Uid); + if (uin) return uin; + uin = (await napCatCore.session.getGroupService().getUinByUids([Uid])).uins.get(Uid); + if (uin) return uin; + uin = (await napCatCore.session.getUixConvertService().getUin([Uid])).uinInfo.get(Uid); + if (uin) return uin; + uin = (await NTQQFriendApi.getBuddyIdMapCache(true)).getKey(Uid);//从Buddy缓存获取Uin + if (uin) return uin; + uin = (await NTQQFriendApi.getBuddyIdMap(true)).getKey(Uid); + if (uin) return uin; + uin = (await NTQQUserApi.getUserDetailInfo(Uid)).uin; //从QQ Native 转换 + return uin; + } + + async getUidByUinV1(Uin: string) { + // 通用转换开始尝试 + let uid = (await napCatCore.session.getUixConvertService().getUid([Uin])).uidInfo.get(Uin); + // Uid 好友转 + if (!uid) { + Array.from(friends.values()).forEach((t) => { + if (t.uin == Uin) { + uid = t.uid; + } + }); + } + //Uid 群友列表转 + if (!uid) { + for (let groupMembersList of groupMembers.values()) { + for (let GroupMember of groupMembersList.values()) { + if (GroupMember.uin == Uin) { + uid = GroupMember.uid; + } + } + } + } + if (!uid) { + let unveifyUid = (await NTQQUserApi.getUserDetailInfoByUin(Uin)).info.uid;//从QQ Native 特殊转换 方法三 + if (unveifyUid.indexOf("*") == -1) { + uid = unveifyUid; + } + } + return uid; + } + async getUinByUidV1(Uid: string) { + let ret = await NTEventDispatch.CallNoListenerEvent + <(Uin: string[]) => Promise<{ uinInfo: Map }>>( + 'NodeIKernelUixConvertService/getUin', + 5000, + [Uid] + ); + let uin = ret.uinInfo.get(Uid); + if (!uin) { + //从Buddy缓存获取Uin + Array.from(friends.values()).forEach((t) => { + if (t.uid == Uid) { + uin = t.uin; + } + }) + } + if (!uin) { + uin = (await NTQQUserApi.getUserDetailInfo(Uid)).uin; //从QQ Native 转换 + } + + // if (!uin) { + // uin = (await NTQQFriendApi.getFriends(false)).find((t) => { t.uid == Uid })?.uin; //从QQ Native 缓存转换 + // } + // if (!uin) { + // uin = (await NTQQFriendApi.getFriends(true)).find((t) => { t.uid == Uid })?.uin; //从QQ Native 非缓存转换 + // } + return uin; + } + async getRecentContactListSnapShot(count: number) { + return await napCatCore.session.getRecentContactService().getRecentContactListSnapShot(count); + } + async getRecentContactListSyncLimit(count: number) { + return await napCatCore.session.getRecentContactService().getRecentContactListSyncLimit(count); + } + async getRecentContactListSync() { + return await napCatCore.session.getRecentContactService().getRecentContactListSync(); + } + async getRecentContactList() { + return await napCatCore.session.getRecentContactService().getRecentContactList(); + } + async getUserDetailInfoByUinV2(Uin: string) { + return await NTEventDispatch.CallNoListenerEvent + <(Uin: string) => Promise>( + 'NodeIKernelProfileService/getUserDetailInfoByUin', + 5000, + Uin + ); + } + async getUserDetailInfoByUin(Uin: string) { + return NTEventDispatch.CallNoListenerEvent + <(Uin: string) => Promise>( + 'NodeIKernelProfileService/getUserDetailInfoByUin', + 5000, + Uin + ); + } + @CacheClassFuncAsync(3600 * 1000, 'ClientKey') + async forceFetchClientKey() { + return await napCatCore.session.getTicketService().forceFetchClientKey(''); + } +} diff --git a/src/core/apis/webapi.ts b/src/core/apis/webapi.ts new file mode 100644 index 00000000..d1dbe4c5 --- /dev/null +++ b/src/core/apis/webapi.ts @@ -0,0 +1,367 @@ +import { selfInfo } from '@/core/data'; +import { log, logDebug } from '@/common/utils/log'; +import { NTQQUserApi } from './user'; +import { RequestUtil } from '@/common/utils/request'; +import { CacheClassFuncAsync } from '@/common/utils/helper'; +export enum WebHonorType { + ALL = 'all', + TALKACTIVE = 'talkative', + PERFROMER = 'performer', + LEGEND = 'legend', + STORONGE_NEWBI = 'strong_newbie', + EMOTION = 'emotion' +} +export interface WebApiGroupMember { + uin: number + role: number + g: number + join_time: number + last_speak_time: number + lv: { + point: number + level: number + } + card: string + tags: string + flag: number + nick: string + qage: number + rm: number +} +interface WebApiGroupMemberRet { + ec: number + errcode: number + em: string + cache: number + adm_num: number + levelname: any + mems: WebApiGroupMember[] + count: number + svr_time: number + max_count: number + search_count: number + extmode: number +} +export interface WebApiGroupNoticeFeed { + u: number//发送者 + fid: string//fid + pubt: number//时间 + msg: { + text: string + text_face: string + title: string, + pics?: { + id: string, + w: string, + h: string + }[] + } + type: number + fn: number + cn: number + vn: number + settings: { + is_show_edit_card: number + remind_ts: number + tip_window_type: number + confirm_required: number + } + read_num: number + is_read: number + is_all_confirm: number +} +export interface WebApiGroupNoticeRet { + ec: number + em: string + ltsm: number + srv_code: number + read_only: number + role: number + feeds: WebApiGroupNoticeFeed[] + group: { + group_id: number + class_ext: number + } + sta: number, + gln: number + tst: number, + ui: any + server_time: number + svrt: number + ad: number +} +interface GroupEssenceMsg { + group_code: string + msg_seq: number + msg_random: number + sender_uin: string + sender_nick: string + sender_time: number + add_digest_uin: string + add_digest_nick: string + add_digest_time: number + msg_content: any[] + can_be_removed: true +} +export interface GroupEssenceMsgRet { + retcode: number + retmsg: string + data: { + msg_list: GroupEssenceMsg[] + is_end: boolean + group_role: number + config_page_url: string + } +} +export class WebApi { + async shareDigest(groupCode: string, msgSeq: string, msgRandom: string, targetGroupCode: string) { + const CookiesObject = await NTQQUserApi.getCookies('qun.qq.com'); + const CookieValue = Object.entries(CookiesObject).map(([key, value]) => `${key}=${value}`).join('; '); + const Bkn = WebApi.genBkn(CookiesObject.skey); + let ret: any = undefined; + const data = 'group_code=' + groupCode + '&msg_seq=' + msgSeq + '&msg_random=' + msgRandom + '&target_group_code=' + targetGroupCode; + const url = 'https://qun.qq.com/cgi-bin/group_digest/share_digest?bkn=' + Bkn + "&" + data; + //console.log(url); + try { + ret = await RequestUtil.HttpGetText(url, 'GET', '', { 'Cookie': CookieValue }); + return ret; + } catch (e) { + return undefined; + } + return undefined; + } + @CacheClassFuncAsync(3600 * 1000, 'webapi_get_group_members') + async getGroupEssenceMsg(GroupCode: string, page_start: string) { + const CookiesObject = await NTQQUserApi.getCookies('qun.qq.com'); + const CookieValue = Object.entries(CookiesObject).map(([key, value]) => `${key}=${value}`).join('; '); + const Bkn = WebApi.genBkn(CookiesObject.skey); + const url = 'https://qun.qq.com/cgi-bin/group_digest/digest_list?bkn=' + Bkn + '&group_code=' + GroupCode + '&page_start=' + page_start + '&page_limit=20'; + let ret; + try { + ret = await RequestUtil.HttpGetJson(url, 'GET', '', { 'Cookie': CookieValue }); + } catch { + return undefined; + } + //console.log(url, CookieValue); + if (ret.retcode !== 0) { + return undefined; + } + return ret; + } + @CacheClassFuncAsync(3600 * 1000, 'webapi_get_group_members') + async getGroupMembers(GroupCode: string, cached: boolean = true): Promise { + //logDebug('webapi 获取群成员', GroupCode); + let MemberData: Array = new Array(); + try { + const CookiesObject = await NTQQUserApi.getCookies('qun.qq.com'); + const CookieValue = Object.entries(CookiesObject).map(([key, value]) => `${key}=${value}`).join('; '); + const Bkn = WebApi.genBkn(CookiesObject.skey); + const retList: Promise[] = []; + const fastRet = await RequestUtil.HttpGetJson('https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?st=0&end=40&sort=1&gc=' + GroupCode + '&bkn=' + Bkn, 'POST', '', { 'Cookie': CookieValue }); + if (!fastRet?.count || fastRet?.errcode !== 0 || !fastRet?.mems) { + return []; + } else { + for (const key in fastRet.mems) { + MemberData.push(fastRet.mems[key]); + } + } + //初始化获取PageNum + const PageNum = Math.ceil(fastRet.count / 40); + //遍历批量请求 + for (let i = 2; i <= PageNum; i++) { + const ret: Promise = RequestUtil.HttpGetJson('https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?st=' + (i - 1) * 40 + '&end=' + i * 40 + '&sort=1&gc=' + GroupCode + '&bkn=' + Bkn, 'POST', '', { 'Cookie': CookieValue }); + retList.push(ret); + } + //批量等待 + for (let i = 1; i <= PageNum; i++) { + const ret = await (retList[i]); + if (!ret?.count || ret?.errcode !== 0 || !ret?.mems) { + continue; + } + for (const key in ret.mems) { + MemberData.push(ret.mems[key]); + } + } + } catch { + return MemberData; + } + return MemberData; + } + // public async addGroupDigest(groupCode: string, msgSeq: string) { + // const url = `https://qun.qq.com/cgi-bin/group_digest/cancel_digest?random=665&X-CROSS-ORIGIN=fetch&group_code=${groupCode}&msg_seq=${msgSeq}&msg_random=444021292`; + // const res = await this.request(url); + // return await res.json(); + // } + + // public async getGroupDigest(groupCode: string) { + // const url = `https://qun.qq.com/cgi-bin/group_digest/digest_list?random=665&X-CROSS-ORIGIN=fetch&group_code=${groupCode}&page_start=0&page_limit=20`; + // const res = await this.request(url); + // return await res.json(); + // } + async setGroupNotice(GroupCode: string, Content: string = '') { + //https://web.qun.qq.com/cgi-bin/announce/add_qun_notice?bkn=${bkn} + //qid=${群号}&bkn=${bkn}&text=${内容}&pinned=0&type=1&settings={"is_show_edit_card":1,"tip_window_type":1,"confirm_required":1} + + const CookiesObject = await NTQQUserApi.getCookies('qun.qq.com'); + const CookieValue = Object.entries(CookiesObject).map(([key, value]) => `${key}=${value}`).join('; '); + const Bkn = WebApi.genBkn(CookiesObject.skey); + let ret: any = undefined; + const data = 'qid=' + GroupCode + '&bkn=' + Bkn + '&text=' + Content + '&pinned=0&type=1&settings={"is_show_edit_card":1,"tip_window_type":1,"confirm_required":1}'; + const url = 'https://web.qun.qq.com/cgi-bin/announce/add_qun_notice?bkn=' + Bkn; + try { + ret = await RequestUtil.HttpGetJson(url, 'GET', '', { 'Cookie': CookieValue }); + return ret; + } catch (e) { + return undefined; + } + return undefined; + } + async getGrouptNotice(GroupCode: string): Promise { + const CookiesObject = await NTQQUserApi.getCookies('qun.qq.com'); + const CookieValue = Object.entries(CookiesObject).map(([key, value]) => `${key}=${value}`).join('; '); + const Bkn = WebApi.genBkn(CookiesObject.skey); + let ret: WebApiGroupNoticeRet | undefined = undefined; + //console.log(CookieValue); + const url = 'https://web.qun.qq.com/cgi-bin/announce/get_t_list?bkn=' + Bkn + '&qid=' + GroupCode + '&ft=23&ni=1&n=1&i=1&log_read=1&platform=1&s=-1&n=20'; + try { + ret = await RequestUtil.HttpGetJson(url, 'GET', '', { 'Cookie': CookieValue }); + if (ret?.ec !== 0) { + return undefined; + } + return ret; + } catch (e) { + return undefined; + } + return undefined; + } + genBkn(sKey: string) { + sKey = sKey || ''; + let hash = 5381; + + for (let i = 0; i < sKey.length; i++) { + const code = sKey.charCodeAt(i); + hash = hash + (hash << 5) + code; + } + + return (hash & 0x7FFFFFFF).toString(); + } + + @CacheClassFuncAsync(3600 * 1000, 'GroupHonorInfo') + async getGroupHonorInfo(groupCode: string, getType: WebHonorType) { + const CookiesObject = await NTQQUserApi.getCookies('qun.qq.com'); + const CookieValue = Object.entries(CookiesObject).map(([key, value]) => `${key}=${value}`).join('; '); + const Bkn = WebApi.genBkn(CookiesObject.skey); + async function getDataInternal(Internal_groupCode: string, Internal_type: number) { + let url = 'https://qun.qq.com/interactive/honorlist?gc=' + Internal_groupCode + '&type=' + Internal_type.toString(); + let res = ''; + let resJson; + try { + res = await RequestUtil.HttpGetText(url, 'GET', '', { 'Cookie': CookieValue }); + const match = res.match(/window\.__INITIAL_STATE__=(.*?);/); + if (match) { + resJson = JSON.parse(match[1].trim()); + } + if (Internal_type === 1) { + return resJson?.talkativeList; + } else { + return resJson?.actorList; + } + } catch (e) { + logDebug('获取当前群荣耀失败', url, e); + } + return undefined; + } + + let HonorInfo: any = { group_id: groupCode }; + + if (getType === WebHonorType.TALKACTIVE || getType === WebHonorType.ALL) { + try { + let RetInternal = await getDataInternal(groupCode, 1); + if (!RetInternal) { + throw new Error('获取龙王信息失败'); + } + HonorInfo.current_talkative = { + user_id: RetInternal[0]?.uin, + avatar: RetInternal[0]?.avatar, + nickname: RetInternal[0]?.name, + day_count: 0, + description: RetInternal[0]?.desc + } + HonorInfo.talkative_list = []; + for (const talkative_ele of RetInternal) { + HonorInfo.talkative_list.push({ + user_id: talkative_ele?.uin, + avatar: talkative_ele?.avatar, + description: talkative_ele?.desc, + day_count: 0, + nickname: talkative_ele?.name + }); + } + } catch (e) { + logDebug(e); + } + } + if (getType === WebHonorType.PERFROMER || getType === WebHonorType.ALL) { + try { + let RetInternal = await getDataInternal(groupCode, 2); + if (!RetInternal) { + throw new Error('获取群聊之火失败'); + } + HonorInfo.performer_list = []; + for (const performer_ele of RetInternal) { + HonorInfo.performer_list.push({ + user_id: performer_ele?.uin, + nickname: performer_ele?.name, + avatar: performer_ele?.avatar, + description: performer_ele?.desc + }); + } + } catch (e) { + logDebug(e); + } + } + if (getType === WebHonorType.PERFROMER || getType === WebHonorType.ALL) { + try { + let RetInternal = await getDataInternal(groupCode, 3); + if (!RetInternal) { + throw new Error('获取群聊炽焰失败'); + } + HonorInfo.legend_list = []; + for (const legend_ele of RetInternal) { + HonorInfo.legend_list.push({ + user_id: legend_ele?.uin, + nickname: legend_ele?.name, + avatar: legend_ele?.avatar, + desc: legend_ele?.description + }); + } + } catch (e) { + logDebug('获取群聊炽焰失败', e); + } + } + if (getType === WebHonorType.EMOTION || getType === WebHonorType.ALL) { + try { + let RetInternal = await getDataInternal(groupCode, 6); + if (!RetInternal) { + throw new Error('获取快乐源泉失败'); + } + HonorInfo.emotion_list = []; + for (const emotion_ele of RetInternal) { + HonorInfo.emotion_list.push({ + user_id: emotion_ele?.uin, + nickname: emotion_ele?.name, + avatar: emotion_ele?.avatar, + desc: emotion_ele?.description + }); + } + } catch (e) { + logDebug('获取快乐源泉失败', e); + } + } + //冒尖小春笋好像已经被tx扬了 + if (getType === WebHonorType.EMOTION || getType === WebHonorType.ALL) { + HonorInfo.strong_newbie_list = []; + } + return HonorInfo; + } +}