diff --git a/packages/napcat-core/apis/flash.ts b/packages/napcat-core/apis/flash.ts new file mode 100644 index 00000000..2d865161 --- /dev/null +++ b/packages/napcat-core/apis/flash.ts @@ -0,0 +1,264 @@ +import { GeneralCallResult, InstanceContext, NapCatCore } from '@/napcat-core'; +import { + createFlashTransferResult, + FileListResponse, + FlashFileSetInfo, + SendStatus, +} from '@/napcat-core/data/flash'; +import { Peer } from '@/napcat-core/types'; + +export class NTQQFlashApi { + context: InstanceContext; + core: NapCatCore; + + constructor (context: InstanceContext, core: NapCatCore) { + this.context = context; + this.core = core; + } + + /** + * 发起闪传上传任务 + * @param fileListToUpload 上传文件绝对路径的列表,可以是文件夹!! + */ + async createFlashTransferUploadTask (fileListToUpload: string[]): Promise < GeneralCallResult & { + createFlashTransferResult: createFlashTransferResult; + seq: number; + } > { + const flashService = this.context.session.getFlashTransferService(); + + const timestamp : number = Date.now(); + const selfInfo = this.core.selfInfo; + + const fileUploadArg = { + screen: 1, // 1 + uploaders: [{ + uin: selfInfo.uin, + uid: selfInfo.uid, + sendEntrance: '', + nickname: selfInfo.nick, + }], + paths: fileListToUpload, + }; + + const uploadResult = await flashService.createFlashTransferUploadTask(timestamp, fileUploadArg); + if (uploadResult.result === 0) { + this.context.logger.log('[Flash] 发起闪传任务成功'); + return uploadResult; + } else { + this.context.logger.logError('[Flash] 发起闪传上传任务失败!!'); + return uploadResult; + } + } + + /** + * 下载闪传文件集 + * @param fileSetId + */ + async downloadFileSetBySetId (fileSetId: string): Promise < GeneralCallResult & { + extraInfo: unknown + } > { + const flashService = this.context.session.getFlashTransferService(); + + const result = await flashService.startFileSetDownload(fileSetId, 1, { isIncludeCompressInnerFiles: false }); // 为了方便,暂时硬编码 + if (result.result === 0) { + this.context.logger.log('[Flash] 成功开始下载文件集'); + } else { + this.context.logger.logError('[Flash] 尝试下载文件集失败!'); + } + return result; + } + + /** + * 获取闪传的外链分享 + * @param fileSetId + */ + async getShareLinkBySetId (fileSetId: string): Promise < GeneralCallResult & { + shareLink: string; + expireTimestamp: string; + }> { + const flashService = this.context.session.getFlashTransferService(); + + const result = await flashService.getShareLinkReq(fileSetId); + if (result.result === 0) { + this.context.logger.log('[Flash] 获取闪传外链分享成功:', result.shareLink); + } else { + this.context.logger.logError('[Flash] 获取闪传外链失败!!'); + } + return result; + } + + /** + * 从分享外链获取文件集id + * @param shareCode + */ + async fromShareLinkFindSetId (shareCode: string): Promise < GeneralCallResult & { + fileSetId: string; + } > { + const flashService = this.context.session.getFlashTransferService(); + + const result = await flashService.getFileSetIdByCode(shareCode); + if (result.result === 0) { + this.context.logger.log('[Flash] 获取shareCode的文件集Id成功!'); + } else { + this.context.logger.logError('[Flash] 获取文件集ID失败!!'); + } + return result; + } + + /** + * 获取fileSet的文件结构信息 (未来可能需要深度遍历) + * == 注意返回结构和其它的不同,没有GeneralCallResult!!! == + * @param fileSetId + */ + async getFileListBySetId (fileSetId: string): Promise < FileListResponse > { + const flashService = this.context.session.getFlashTransferService(); + + const requestArg = { + seq: 0, + fileSetId, + isUseCache: false, + sceneType: 1, // 硬编码 + reqInfos: [ + { + count: 18, // 18 ?? + paginationInfo: {}, + parentId: '', + reqIndexPath: '', + reqDepth: 1, + filterCondition: { + fileCategory: 0, + filterType: 0, + }, + sortConditions: [ + { + sortField: 0, + sortOrder: 0, + }, + ], + isNeedPhysicalInfoReady: false, + }, + ], + }; + const result = await flashService.getFileList(requestArg); + if (result.rsp.result === 0) { + this.context.logger.log('[Flash] 获取fileSet文件信息成功!'); + return result.rsp; + } else { + this.context.logger.logError(`[Flash] 获取文件信息失败:ErrMsg: ${result.rsp.errMs}`); + return result.rsp; + } + } + + /** + * 获取闪传文件集合信息 + * @param fileSetId + */ + async getFileSetIndoBySetId (fileSetId: string): Promise < GeneralCallResult & { + seq: number; + isCache: boolean; + fileSet: FlashFileSetInfo; + } > { + const flashService = this.context.session.getFlashTransferService(); + + const requestArg = { + fileSetId, + }; + + const result = await flashService.getFileSet(requestArg); + if (result.result === 0) { + this.context.logger.log('[Flash] 获取闪传文件集信息成功!'); + } else { + this.context.logger.logError('[Flash] 获取闪传文件信息失败!!'); + } + return result; + } + + /** + * 发送闪传消息(私聊/群聊) + * @param fileSetId + * @param peer + */ + async sendFlashMessage (fileSetId: string, peer:Peer): Promise < { + errCode: number, + errMsg: string, + rsp: { + sendStatus: SendStatus[] + } + } > { + const flashService = this.context.session.getFlashTransferService(); + + const target = { + destUid: peer.peerUid, + destType: peer.chatType, + // destUin: peer.peerUin, + }; + + const requestsArg = { + fileSetId, + targets: [target], + }; + + const result = await flashService.sendFlashTransferMsg(requestsArg); + if (result.errCode === 0) { + this.context.logger.log('[Flash] 消息发送成功'); + } else { + this.context.logger.logError(`[Flash] 消息发送失败!!原因:${result.errMsg}`); + } + return result; + } + + /** + * 获取闪传文件集中某个文件的下载URL(外链) + * @param fileSetId + * @param options + */ + async getFileTransUrl (fileSetId: string, options: { fileName?: string; fileIndex?: number }): Promise < GeneralCallResult & { + transferUrl: string; + } > { + const flashService = this.context.session.getFlashTransferService(); + const result = await this.getFileListBySetId(fileSetId); + + const { fileName, fileIndex } = options; + + let targetFile: any; + let file: any; + + const allFolder = result.fileLists; + + // eslint-disable-next-line no-labels + searchLoop: for (const folder of allFolder) { + const fileList = folder.fileList; + for (let i = 0; i < fileList.length; i++) { + file = fileList[i]; + + if (fileName !== undefined && file.name === fileName) { + targetFile = file; + // eslint-disable-next-line no-labels + break searchLoop; + } + + if (fileIndex !== undefined && i === fileIndex) { + targetFile = file; + // eslint-disable-next-line no-labels + break searchLoop; + } + } + } + if (targetFile === undefined) { + this.context.logger.logError('[Flash] 未找到对应文件!!'); + return { + result: -1, + errMsg: '未找到对应文件', + transferUrl: '', + }; + } else { + this.context.logger.log('[Flash] 找到对应文件,准备尝试获取传输链接'); + const res = await flashService.startFileTransferUrl(targetFile); + return { + result: 0, + errMsg: '', + transferUrl: res.url, + }; + } + } +} diff --git a/packages/napcat-core/apis/index.ts b/packages/napcat-core/apis/index.ts index 1a423a5d..9659883f 100644 --- a/packages/napcat-core/apis/index.ts +++ b/packages/napcat-core/apis/index.ts @@ -7,3 +7,5 @@ export * from './webapi'; export * from './system'; export * from './packet'; export * from './file'; +export * from './online'; +export * from './flash'; diff --git a/packages/napcat-core/apis/msg.ts b/packages/napcat-core/apis/msg.ts index 56e8586e..0bf23cfe 100644 --- a/packages/napcat-core/apis/msg.ts +++ b/packages/napcat-core/apis/msg.ts @@ -245,7 +245,7 @@ export class NTQQMsgApi { [ '0', peer, - msgElements, + msgElements as any, new Map(), ], (ret) => ret.result === 0, diff --git a/packages/napcat-core/apis/online.ts b/packages/napcat-core/apis/online.ts new file mode 100644 index 00000000..235ae978 --- /dev/null +++ b/packages/napcat-core/apis/online.ts @@ -0,0 +1,239 @@ +import { InstanceContext, NapCatCore } from '@/napcat-core'; +import { Peer } from '@/napcat-core/types'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { GeneralCallResultStatus } from '@/napcat-core/services/common'; +import { sleep } from '@/napcat-common/src/helper'; + +const normalizePath = (p: string) => path.normalize(p).toLowerCase(); + +export class NTQQOnlineApi { + context: InstanceContext; + core: NapCatCore; + + constructor (context: InstanceContext, core: NapCatCore) { + this.context = context; + this.core = core; + } + + /** + * 这里不等待node返回,因为the fuck wrapper.node 根本不返回(会卡死不知道为什么)!!! 只能手动查询判断死活 + * @param peer + * @param filePath + * @param fileName + */ + async sendOnlineFile (peer: Peer, filePath: string, fileName: string): Promise { + if (!fs.existsSync(filePath)) { + throw new Error(`[NapCat] 文件不存在: ${filePath}`); + } + const actualFileName = fileName || path.basename(filePath); + const fileSize = fs.statSync(filePath).size.toString(); + + const fileElementToSend = [{ + elementType: 23, + fileElement: { + fileName: actualFileName, + filePath, + fileSize, + }, + }]; + + const msgService = this.context.session.getMsgService(); + const startTime = Math.floor(Date.now() / 1000) - 2; // 容错时间窗口 + + msgService.sendMsg('0', peer, fileElementToSend, new Map()).catch((_e: any) => { + }); + + const maxRetries = 10; + let retryCount = 0; + + while (retryCount < maxRetries) { + await sleep(1000); + retryCount++; + + try { + const msgListResult = await msgService.getOnlineFileMsgs(peer); + + const msgs = msgListResult?.msgList || []; + + const foundMsg = msgs.find((msg: any) => { + if (parseInt(msg.msgTime) < startTime) return false; + + const validElement = msg.elements.find((el: any) => { + if (el.elementType !== 23 || !el.fileElement) return false; + + const isNameMatch = el.fileElement.fileName === actualFileName; + const isPathMatch = normalizePath(el.fileElement.filePath) === normalizePath(filePath); + + return isNameMatch && isPathMatch; + }); + + return !!validElement; + }); + + if (foundMsg) { + const targetElement = foundMsg.elements.find((el: any) => el.elementType === 23); + this.context.logger.log('[OnlineFile] 在线文件发送成功!'); + return { + result: GeneralCallResultStatus.OK, + errMsg: '', + msgId: foundMsg.msgId, + elementId: targetElement?.elementId || '', + }; + } + } catch (_e) { + } + } + this.context.logger.logError('[OnlineFile] 在线文件发送失败!!!'); + return { + result: GeneralCallResultStatus.ERROR, + errMsg: '[NapCat] Send Online File Timeout: Message not found in history.', + }; + } + + /** + * 发送在线文件夹 + * @param peer + * @param folderPath + * @param folderName + */ + async sendOnlineFolder (peer: Peer, folderPath: string, folderName?: string): Promise { + const actualFolderName = folderName || path.basename(folderPath); + + if (!fs.existsSync(folderPath)) { + return { result: GeneralCallResultStatus.ERROR, errMsg: `Folder not found: ${folderPath}` }; + } + + if (!fs.statSync(folderPath).isDirectory()) { + return { result: GeneralCallResultStatus.ERROR, errMsg: `Path is not a directory: ${folderPath}` }; + } + const folderElementItem = { + elementType: 30, + elementId: '', + fileElement: { + fileName: actualFolderName, + filePath: folderPath, + }, + } as any; + + const msgService = this.context.session.getMsgService(); + const startTime = Math.floor(Date.now() / 1000) - 2; + msgService.sendMsg('0', peer, [folderElementItem], new Map()).catch((_e: any) => { + + }); + + const maxRetries = 10; + let retryCount = 0; + + while (retryCount < maxRetries) { + await sleep(1000); + retryCount++; + + try { + const msgListResult = await msgService.getOnlineFileMsgs(peer); + const msgs = msgListResult?.msgList || []; + + const foundMsg = msgs.find((msg: any) => { + if (parseInt(msg.msgTime) < startTime) return false; + + const validElement = msg.elements.find((el: any) => { + if (el.elementType !== 30 || !el.fileElement) return false; + + const isNameMatch = el.fileElement.fileName === actualFolderName; + const isPathMatch = normalizePath(el.fileElement.filePath) === normalizePath(folderPath); + + return isNameMatch && isPathMatch; + }); + return !!validElement; + }); + + if (foundMsg) { + const targetElement = foundMsg.elements.find((el: any) => el.elementType === 30); + this.context.logger.log('[OnlineFile] 在线文件夹发送成功!'); + return { + result: GeneralCallResultStatus.OK, + errMsg: '', + msgId: foundMsg.msgId, + elementId: targetElement?.elementId || '', + }; + } + } catch (_e) { + + } + } + this.context.logger.logError('[OnlineFile] 在线文件发送失败!!!'); + return { + result: GeneralCallResultStatus.ERROR, + errMsg: '[NapCat] Send Online Folder Timeout: Message not found in history.', + }; + } + + /** + * 获取好友的在线文件消息 + * @param peer + */ + async getOnlineFileMsg (peer: Peer) : Promise { + const msgService = this.context.session.getMsgService(); + return await msgService.getOnlineFileMsgs(peer); + } + + /** + * 取消在线文件的发送 + * @param peer + * @param msgId + */ + async cancelMyOnlineFileMsg (peer: Peer, msgId: string) : Promise { + const msgService = this.context.session.getMsgService(); + await msgService.cancelSendMsg(peer, msgId); + } + + /** + * 拒绝接收在线文件 + * @param peer + * @param msgId + * @param elementId + */ + async refuseOnlineFileMsg (peer: Peer, msgId: string, elementId: string) : Promise { + const msgService = this.context.session.getMsgService(); + const arrToSend = { + msgId, + peerUid: peer.peerUid, + chatType: 1, + elementId, + downloadType: 1, + downSourceType: 1, + }; + + await msgService.refuseGetRichMediaElement(arrToSend); + } + + /** + * 接收在线文件/文件夹 + * @param peer + * @param msgId + * @param elementId + * @constructor + */ + async receiveOnlineFileOrFolder (peer: Peer, msgId: string, elementId: string) : Promise { + const msgService = this.context.session.getMsgService(); + const arrToSend = { + msgId, + peerUid: peer.peerUid, + chatType: 1, + elementId, + downSourceType: 1, + downloadType: 1, + }; + return await msgService.getRichMediaElement(arrToSend); + } + + /** + * 在线文件/文件夹转离线 + * @param peer + * @param msgId + */ + async switchFileToOffline (peer: Peer, msgId: string) : Promise { + const msgService = this.context.session.getMsgService(); + await msgService.switchToOfflineSendMsg(peer, msgId); + } +} diff --git a/packages/napcat-core/data/flash.ts b/packages/napcat-core/data/flash.ts new file mode 100644 index 00000000..56abc817 --- /dev/null +++ b/packages/napcat-core/data/flash.ts @@ -0,0 +1,324 @@ +export interface FlashBaseRequest { + fileSetId: string +} + +export interface UploaderInfo { + uin: string, + nickname: string, + uid: string, + sendEntrance: string, // "" +} + +export interface thumbnailInfo { + id: string, + url: { + spec: number, + uri: string, + }[], + localCachePath: string, +} + +export interface SendTarget { + destType: number // 1私聊 + destUin?: string, + destUid: string, +} + +export interface SendTargetRequests { + fileSetId: string + targets: SendTarget[] +} + +export interface DownloadStatusInfo { + result: number; // 0 + fileSetId: string; + status: number; + info: { + curDownLoadFailFileNum: number, + curDownLoadedPauseFileNum: number, + curDownLoadedFileNum: number, + curRealDownLoadedFileNum: number, + curDownloadingFileNum: number, + totalDownLoadedFileNum: number, + curDownLoadedBytes: string, // "0" + totalDownLoadedBytes: string, + curSpeedBps: number, + avgSpeedBps: number, + maxSpeedBps: number, + remainDownLoadSeconds: number, + failFileIdList: [], + allFileIdList: [], + hasNormalFileDownloading: boolean, + onlyCompressInnerFileDownloading: boolean, + isAllFileAlreadyDownloaded: boolean, + saveFileSetDir: string, + allWaitingStatusTask: boolean, + downloadSceneType: number, + retryCount: number, + statisticInfo: { + downloadTaskId: string, + downloadFilesetName: string, + downloadFileTypeDistribution: string, + downloadFileSizeDistribution: string + }, + albumStorageFailImageNum: number, + albumStorageFailVideoNum: number, + albumStorageFailFileIdList: [], + albumStorageSucImageNum: number, + albumStorageSucVideoNum: number, + albumStorageSucFileIdList: [], + albumStorageFileNum: number + } +} + +export interface physicalInfo { + id: string, + url: string, + status: number, // 2 已下载 + processing: string, + localPath: string, + width: 0, + height: 0, + time: number, +} + +export interface downloadInfo { + status: number, + curDownLoadBytes: string, + totalFileBytes: string, + errorCode: number, +} + +export interface uploadInfo { + uploadedBytes: string, + errorCode: number, + svrRrrCode: number, + errMsg: string, + isNeedDelDeviceInfo: boolean, + thumbnailUploadState: number + isSecondHit: boolean, + hasModifiedErr: boolean, +} + +export interface folderUploadInfo { + totalUploadedFileSize: string + successCount: number + failedCount: number +} + +export interface folderDownloadInfo { + totalDownloadedFileSize: string + totalFileSize: string + totalDownloadFileCount: number + successCount: number + failedCount: number + pausedCount: number + cancelCount: number + downloadingCount: number + partialDownloadCount: number + curLevelDownloadedFileCount: number + curLevelUnDownloadedFileCount: number +} + +export interface compressFileFolderInfo { + downloadStatus: number + saveFileDirPath: string + totalFileCount: string + totalFileSize: string +} + +export interface albumStorgeInfo { + status: number + localIdentifier: string + errorCode: number + timeCost: number +} + +export interface FlashOneFileInfo { + fileSetId: string + cliFileId: string // client?? 或许可以换取url + compressedFileFolderId: string + archiveIndex: 0 + indexPath: string + isDir: boolean // 文件或者文件夹!! + parentId: string + depth: number // 1 + cliFileIndex: number + fileType: number // 枚举!! 已完成枚举!! + name: string + namePinyin: string + isCover: boolean + isCoverOriginal: boolean + fileSize: string + fileCount: number + thumbnail: thumbnailInfo + physical: physicalInfo + srvFileId: string // service?? 服务器上面的id吗? + srvParentFileId: string + svrLastUpdateTimestamp: string + downloadInfo: downloadInfo + saveFilePath: string + search_relative_path: string + disk_relative_path: string + uploadInfo: uploadInfo + status: number + uploadStatus: number // 3已上传成功 + downloadStatus: number // 0未下载 + folderUploadInfo: folderUploadInfo + folderDownloadInfo: folderDownloadInfo + sha1: string + bookmark: string + compressFileFolderInfo: compressFileFolderInfo + uploadPauseReason: string + downloadPauseReason: string + filePhysicalSize: string + thumbnail_sha1: string | null + thumbnail_size: string | null + needAlbumStorage: boolean + albumStorageInfo: albumStorgeInfo +} + +export interface fileListsInfo { + parentId: string, + depth: number, // 1 + fileList: FlashOneFileInfo[], + paginationInfo: {} + isEnd: boolean, + isCache: boolean, +} + +export interface FileListResponse { + seq: number, + result: number, + errMs: string, + fileLists: fileListsInfo[], +} + +export interface createFlashTransferResult { + fileSetId: string, + shareLink: string, + expireTime: string, + expireLeftTime: string, +} + +export interface StartFlashTaskRequests { + screen?: number; // 1 PC-QQ + uploaders: UploaderInfo[]; + permission?: {}; + coverPath?: string; + paths: string[]; // 文件的绝对路径,可以是文件夹 + // excludePaths: []; + // expireLeftTime: 0, + // isNeedDelDeviceInfo: boolean, + // isNeedDelLocation: boolean, + // coverOriginalInfos: [], + // uploadSceneType: 10, // 不知道怎么枚举 先硬编码吧 + // detectPrivacyInfoResult: { + // exists: boolean, + // allDetectResults: {} + // } +} + +export interface FileListInfoRequests { + seq: number, // 0 + fileSetId: string, + isUseCache: boolean, + sceneType: number, // 1 + reqInfos: { + count: number, // 18 ?? 硬编码吧 不懂 + paginationInfo: {}, + parentId: string, + reqIndexPath: string, + reqDepth: number, // 1 + filterCondition: { + fileCategory: number, + filterType: number, + }, // 0 + sortConditions: { + sortField: number, + sortOrder: number, + }[], + isNeedPhysicalInfoReady: boolean + }[] +} + +export interface FlashFileSetInfo { + fileSetId: string, + name: string, + namePinyin: string, + totalFileCount: number, + totalFileSize: number, + permission: {}, + shareInfo: { + shareLink: string, + extractionCode: string, + }, + cover: { + id: string, + urls: [ + { + spec: number, // 2 + url: string + } + ], + localCachePath: string + }, + uploaders: [ + { + uin: string, + nickname: string, + uid: string, + sendEntrance: string + } + ], + expireLeftTime: number, + aiClusteringStatus: { + firstClusteringList: [], + shouldPull: boolean + }, + createTime: number, + expireTime: number, + firstLevelItemCount: 1, + svrLastUpdateTimestamp: 0, + taskId: string, // 同 fileSetId + uploadInfo: { + totalUploadedFileSize: number, + successCount: number, + failedCount: number + }, + downloadInfo: { + totalDownloadedFileSize: 0, + totalFileSize: 0, + totalDownloadFileCount: 0, + successCount: 0, + failedCount: 0, + pausedCount: 0, + cancelCount: 0, + status: 0, + curLevelDownloadedFileCount: number, + curLevelUnDownloadedFileCount: 0 + }, + transferType: number, + isLocalCreate: true, + status: number, // todo 枚举全部状态 + uploadStatus: number, // todo 同上 + uploadPauseReason: 0, + downloadStatus: 0, + downloadPauseReason: 0, + saveFileSetDir: string, + uploadSceneType: 10, + downloadSceneType: 0, // 0 PC-QQ 103 web + retryCount: number, + isMergeShareUpload: 0, + isRemoveDeviceInfo: boolean, + isRemoveLocation: boolean +} + +export interface SendStatus { + result: number, + msg: string, + target: { + destType: number, + destUid: string, + } +} diff --git a/packages/napcat-core/helper/log.ts b/packages/napcat-core/helper/log.ts index ba200f68..99579927 100644 --- a/packages/napcat-core/helper/log.ts +++ b/packages/napcat-core/helper/log.ts @@ -5,6 +5,7 @@ import fs from 'node:fs/promises'; import { NTMsgAtType, ChatType, ElementType, MessageElement, RawMessage, SelfInfo } from '@/napcat-core/index'; import { ILogWrapper } from 'napcat-common/src/log-interface'; import EventEmitter from 'node:events'; + export enum LogLevel { DEBUG = 'debug', INFO = 'info', @@ -263,7 +264,13 @@ function msgElementToText (element: MessageElement, msg: RawMessage, recursiveLe } if (element.fileElement) { - return `[文件 ${element.fileElement.fileName}]`; + if (element.fileElement.fileUuid) { + return `[文件 ${element.fileElement.fileName}]`; + } else if (element.elementType === ElementType.TOFURECORD) { + return `[在线文件 ${element.fileElement.fileName}]`; + } else if (element.elementType === ElementType.ONLINEFOLDER) { + return `[在线文件夹 ${element.fileElement.fileName}/]`; + } } if (element.videoElement) { @@ -287,7 +294,12 @@ function msgElementToText (element: MessageElement, msg: RawMessage, recursiveLe } if (element.markdownElement) { - return '[Markdown 消息]'; + // console.log(element.markdownElement); + if (element.markdownElement.mdSummary !== undefined && element.markdownElement.mdExtInfo !== undefined && element.markdownElement.mdExtInfo.flashTransferInfo) { + return element.markdownElement.mdSummary; + } else { + return '[Markdown 消息]'; + } } if (element.multiForwardMsgElement) { @@ -296,6 +308,8 @@ function msgElementToText (element: MessageElement, msg: RawMessage, recursiveLe if (element.elementType === ElementType.GreyTip) { return '[灰条消息]'; + } else if (element.elementType === ElementType.FILE) { + return '[文件发送中]'; } return `[未实现 (ElementType = ${element.elementType})]`; diff --git a/packages/napcat-core/index.ts b/packages/napcat-core/index.ts index b92e141d..e444ea9b 100644 --- a/packages/napcat-core/index.ts +++ b/packages/napcat-core/index.ts @@ -6,6 +6,8 @@ import { NTQQSystemApi, NTQQUserApi, NTQQWebApi, + NTQQFlashApi, + NTQQOnlineApi, } from '@/napcat-core/apis'; import { NTQQCollectionApi } from '@/napcat-core/apis/collection'; import { @@ -23,7 +25,7 @@ import path from 'node:path'; import fs from 'node:fs'; import { hostname, systemName, systemVersion } from 'napcat-common/src/system'; import { NTEventWrapper } from '@/napcat-core/helper/event'; -import { KickedOffLineInfo, SelfInfo, SelfStatusInfo } from '@/napcat-core/types'; +import { KickedOffLineInfo, RawMessage, SelfInfo, SelfStatusInfo } from '@/napcat-core/types'; import { NapCatConfigLoader, NapcatConfigSchema } from '@/napcat-core/helper/config'; import os from 'node:os'; import { NodeIKernelMsgListener, NodeIKernelProfileListener } from '@/napcat-core/listeners'; @@ -123,6 +125,8 @@ export class NapCatCore { MsgApi: new NTQQMsgApi(this.context, this), UserApi: new NTQQUserApi(this.context, this), GroupApi: new NTQQGroupApi(this.context, this), + FlashApi: new NTQQFlashApi(this.context, this), + OnlineApi: new NTQQOnlineApi(this.context, this), }; container.bind(NapCatCore).toConstantValue(this); container.bind(TypedEventEmitter).toConstantValue(this.event); @@ -178,6 +182,11 @@ export class NapCatCore { async initNapCatCoreListeners () { const msgListener = new NodeIKernelMsgListener(); + // 在线文件/文件夹消息 + msgListener.onRecvOnlineFileMsg = (msgs: RawMessage[]) => { + msgs.forEach(msg => this.context.logger.logMessage(msg, this.selfInfo)); + }; + msgListener.onKickedOffLine = (Info: KickedOffLineInfo) => { // 下线通知 const tips = `[KickedOffLine] [${Info.tipsTitle}] ${Info.tipsDesc}`; @@ -297,4 +306,6 @@ export interface StableNTApiWrapper { MsgApi: NTQQMsgApi, UserApi: NTQQUserApi, GroupApi: NTQQGroupApi; + FlashApi: NTQQFlashApi, + OnlineApi: NTQQOnlineApi, } diff --git a/packages/napcat-core/services/NodeIKernelFlashTransferService.ts b/packages/napcat-core/services/NodeIKernelFlashTransferService.ts new file mode 100644 index 00000000..e7c687ad --- /dev/null +++ b/packages/napcat-core/services/NodeIKernelFlashTransferService.ts @@ -0,0 +1,302 @@ +import { GeneralCallResult } from './common'; +import { + SendStatus, + StartFlashTaskRequests, + createFlashTransferResult, + FlashBaseRequest, + FlashFileSetInfo, + FileListInfoRequests, + FileListResponse, + DownloadStatusInfo, + SendTargetRequests, + FlashOneFileInfo, +} from '../data/flash'; + +export interface NodeIKernelFlashTransferService { + /** + * 开始闪传服务 并上传文件/文件夹(可以多选,非常好用) + * @param timestamp + * @param fileInfo + */ + createFlashTransferUploadTask(timestamp: number, fileInfo: StartFlashTaskRequests): Promise < GeneralCallResult & { + createFlashTransferResult: createFlashTransferResult; + seq: number; + } >; // 2 arg 重点 // 自动上传 + + createMergeShareTask(...args: unknown[]): unknown; // 2 arg + + updateFlashTransfer(...args: unknown[]): unknown; // 2 arg + + getFileSetList(...args: unknown[]): unknown; // 1 arg + + getFileSetListCount(...args: unknown[]): unknown; // 1 arg + + /** + * 获取file set 的信息 + * @param fileSetIdDict + */ + getFileSet(fileSetIdDict: FlashBaseRequest): Promise < GeneralCallResult & { + seq: number; + isCache: boolean; + fileSet: FlashFileSetInfo; + } >; // 1 arg + + /** + * 获取file set 里面的文件信息(文件夹结构) + * @param requestArgs + */ + getFileList(requestArgs: FileListInfoRequests): Promise < { + rsp: FileListResponse; + } > ; // 1 arg 这个方法QQ有bug??? 并没有,是我参数有问题 + + getDownloadedFileCount(...args: unknown[]): unknown; // 1 arg + + getLocalFileList(...args: unknown[]): unknown; // 3 arg + + batchRemoveUserFileSetHistory(...args: unknown[]): unknown; // 1 arg + + /** + * 获取分享链接 + * @param fileSetId + */ + getShareLinkReq(fileSetId:string): Promise< GeneralCallResult & { + shareLink: string; + expireTimestamp: string; + }>; + + /** + * 由分享链接到fileSetId + * @param shareCode + */ + getFileSetIdByCode(shareCode: string): Promise < GeneralCallResult & { + fileSetId: string; + } > ; // 1 arg code == share code + + batchRemoveFile(...args: unknown[]): unknown; // 1 arg + + checkUploadPathValid(...args: unknown[]): unknown; // 1 arg + + cleanFailedFiles(...args: unknown[]): unknown; // 2 arg + + /** + * 暂停所有的任务 + */ + resumeAllUnfinishedTasks(): unknown; // 0 arg !! + + addFileSetUploadListener(...args: unknown[]): unknown; // 1 arg + + removeFileSetUploadListener(...args: unknown[]): unknown; // 1 arg + + /** + * 开始上传任务 适用于已暂停的 + * @param fileSetId + */ + startFileSetUpload(fileSetId: string): void; // 1 arg 并不是新建任务,应该是暂停后的启动 + + /** + * 结束,无法再次启动 + * @param fileSetId + */ + stopFileSetUpload(fileSetId: string): void; // 1 arg stop 后start无效 + + /** + * 暂停上传 + * @param fileSetId + */ + pauseFileSetUpload(fileSetId: string): void; // 1 arg 暂停上传 + + /** + * 继续上传 + * @param args + */ + resumeFileSetUpload(...args: unknown[]): unknown; // 1 arg 继续 + + pauseFileUpload(...args: unknown[]): unknown; // 1 arg + + resumeFileUpload(...args: unknown[]): unknown; // 1 arg + + stopFileUpload(...args: unknown[]): unknown; // 1 arg + + asyncGetThumbnailPath(...args: unknown[]): unknown; // 2 arg + + setDownLoadDefaultFileDir(...args: unknown[]): unknown; // 1 arg + + setFileSetDownloadDir(...args: unknown[]): unknown; // 2 arg + + getFileSetDownloadDir(...args: unknown[]): unknown; // 1 arg + + setFlashTransferDir(...args: unknown[]): unknown; // 2 arg + + addFileSetDownloadListener(...args: unknown[]): unknown; // 1 arg + + removeFileSetDownloadListener(...args: unknown[]): unknown; // 1 arg + + /** + * 开始下载file set的函数 同开始上传 + * @param fileSetId + * @param chatType 聊天类型 //因为没有peer,其实可以硬编码为1 (好友私聊) + * @param arg // 默认为false + */ + startFileSetDownload(fileSetId:string, chatType: number, arg: { isIncludeCompressInnerFiles: boolean }): Promise < GeneralCallResult & { + extraInfo: 0 + } >; // 3 arg + + stopFileSetDownload(fileSetId: string, arg1: { isIncludeCompressInnerFiles: boolean }): Promise < GeneralCallResult & { + extraInfo: 0 + } > ; // 2 arg 结束不可重启!! + + pauseFileSetDownload(fileSetId: string, arg1: { isIncludeCompressInnerFiles: boolean }): Promise < GeneralCallResult & { + extraInfo: 0 + } > ; // 2 arg + + resumeFileSetDownload(fileSetId: string, arg1: { isIncludeCompressInnerFiles: boolean }): Promise < GeneralCallResult & { + extraInfo: 0 + } > ; // 2 arg + + startFileListDownLoad(...args: unknown[]): unknown; // 4 arg // 大概率是选择set里面的部分文件进行下载,没必要,不想写 + + pauseFileListDownLoad(...args: unknown[]): unknown; // 2 arg + + resumeFileListDownLoad(...args: unknown[]): unknown; // 2 arg + + stopFileListDownLoad(...args: unknown[]): unknown; // 2 arg + + startThumbnailListDownload(fileSetId: string): Promise < GeneralCallResult >; // 1 arg // 缩略图下载 + + stopThumbnailListDownload(fileSetId: string): Promise < GeneralCallResult >; // 1 arg + + asyncRequestDownLoadStatus(fileSetId: string): Promise < DownloadStatusInfo >; // 1 arg + + startFileTransferUrl(fileInfo: FlashOneFileInfo): Promise < { + ret: number, + url: string, + expireTimestampSeconds: string + } >; // 1 arg + + startFileListDownLoadBySessionId(...args: unknown[]): unknown; // 2 arg + + addFileSetSimpleStatusListener(...args: unknown[]): unknown; // 2 arg + + addFileSetSimpleStatusMonitoring(...args: unknown[]): unknown; // 2 arg + + removeFileSetSimpleStatusMonitoring(...args: unknown[]): unknown; // 2 arg + + removeFileSetSimpleStatusListener(...args: unknown[]): unknown; // 1 arg + + addDesktopFileSetSimpleStatusListener(...args: unknown[]): unknown; // 1 arg + + addDesktopFileSetSimpleStatusMonitoring(...args: unknown[]): unknown; // 1 arg + + removeDesktopFileSetSimpleStatusMonitoring(...args: unknown[]): unknown; // 1 arg + + removeDesktopFileSetSimpleStatusListener(...args: unknown[]): unknown; // 1 arg + + addFileSetSimpleUploadInfoListener(...args: unknown[]): unknown; // 1 arg + + addFileSetSimpleUploadInfoMonitoring(...args: unknown[]): unknown; // 1 arg + + removeFileSetSimpleUploadInfoMonitoring(...args: unknown[]): unknown; // 1 arg + + removeFileSetSimpleUploadInfoListener(...args: unknown[]): unknown; // 1 arg + /** + * 发送闪传消息 + * @param sendArgs + */ + sendFlashTransferMsg(sendArgs: SendTargetRequests): Promise < { + errCode: number, + errMsg: string, + rsp: { + sendStatus: SendStatus[] + } + } >; // 1 arg 估计是file set id + + addFlashTransferTaskInfoListener(...args: unknown[]): unknown; // 1 arg + + removeFlashTransferTaskInfoListener(...args: unknown[]): unknown; // 1 arg + + retrieveLocalLastFailedSetTasksInfo(): unknown; // 0 arg + + getFailedFileList(fileSetId: string): Promise < { + rsp: { + seq: number; + result: number; + errMs: string; + fileSetId: string; + fileList: [] + } + } >; // 1 arg + + getLocalFileListByStatuses(...args: unknown[]): unknown; // 1 arg + + addTransferStateListener(...args: unknown[]): unknown; // 1 arg + + removeTransferStateListener(...args: unknown[]): unknown; // 1 arg + + getFileSetFirstClusteringList(...args: unknown[]): unknown; // 3 arg + + getFileSetClusteringList(...args: unknown[]): unknown; // 1 arg + + addFileSetClusteringListListener(...args: unknown[]): unknown; // 1 arg + + removeFileSetClusteringListListener(...args: unknown[]): unknown; // 1 arg + + getFileSetClusteringDetail(...args: unknown[]): unknown; // 1 arg + + doAIOFlashTransferBubbleActionWithStatus(...args: unknown[]): unknown; // 4 arg + + getFilesTransferProgress(...args: unknown[]): unknown; // 1 arg + + pollFilesTransferProgress(...args: unknown[]): unknown; // 1 arg + + cancelPollFilesTransferProgress(...args: unknown[]): unknown; // 1 arg + + checkDownloadStatusBeforeLocalFileOper(...args: unknown[]): unknown; // 3 arg + + getCompressedFileFolder(...args: unknown[]): unknown; // 1 arg + + addFolderListener(...args: unknown[]): unknown; // 1 arg + + removeFolderListener(...args: unknown[]): unknown; + + addCompressedFileListener(...args: unknown[]): unknown; + + removeCompressedFileListener(...args: unknown[]): unknown; + + getFileCategoryList(...args: unknown[]): unknown; + + addDeviceStatusListener(...args: unknown[]): unknown; + + removeDeviceStatusListener(...args: unknown[]): unknown; + + checkDeviceStatus(...args: unknown[]): unknown; + + pauseAllTasks(...args: unknown[]): unknown; // 2 arg + + resumePausedTasksAfterDeviceStatus(...args: unknown[]): unknown; + + onSystemGoingToSleep(...args: unknown[]): unknown; + + onSystemWokeUp(...args: unknown[]): unknown; + + getFileMetas(...args: unknown[]): unknown; + + addDownloadCntStatisticsListener(...args: unknown[]): unknown; + + removeDownloadCntStatisticsListener(...args: unknown[]): unknown; + + detectPrivacyInfoInPaths(...args: unknown[]): unknown; + + getFileThumbnailUrl(...args: unknown[]): unknown; + + handleDownloadFinishAfterSaveToAlbum(...args: unknown[]): unknown; + + checkBatchFilesDownloadStatus(...args: unknown[]): unknown; + + onCheckAlbumStorageStatusResult(...args: unknown[]): unknown; + + addFileAlbumStorageListener(...args: unknown[]): unknown; + + removeFileAlbumStorageListener(...args: unknown[]): unknown; + + refreshFolderStatus(...args: unknown[]): unknown; +} diff --git a/packages/napcat-core/services/NodeIKernelMsgService.ts b/packages/napcat-core/services/NodeIKernelMsgService.ts index 9ed9b545..f64cf99b 100644 --- a/packages/napcat-core/services/NodeIKernelMsgService.ts +++ b/packages/napcat-core/services/NodeIKernelMsgService.ts @@ -1,4 +1,4 @@ -import { ElementType, MessageElement, Peer, RawMessage, SendMessageElement } from '@/napcat-core/types'; +import { ElementType, MessageElement, Peer, RawMessage, FileElement } from '@/napcat-core/types'; import { NodeIKernelMsgListener } from '@/napcat-core/listeners/NodeIKernelMsgListener'; import { GeneralCallResult } from '@/napcat-core/services/common'; import { MsgReqType, QueryMsgsParams, TmpChatInfoApi } from '@/napcat-core/types/msg'; @@ -10,7 +10,10 @@ export interface NodeIKernelMsgService { addKernelMsgListener(nodeIKernelMsgListener: NodeIKernelMsgListener): number; - sendMsg(msgId: string, peer: Peer, msgElements: SendMessageElement[], map: Map): Promise; + sendMsg(msgId: string, peer: Peer, msgElements: { + elementType: number; + fileElement: { fileName: string; filePath: string; fileSize: string } + }[], map: Map): Promise; recallMsg(peer: Peer, msgIds: string[]): Promise; @@ -70,7 +73,7 @@ export interface NodeIKernelMsgService { addSendMsg(...args: unknown[]): unknown; - cancelSendMsg(...args: unknown[]): unknown; + cancelSendMsg(peer: Peer, msgId: string): Promise ; switchToOfflineSendMsg(peer: Peer, MsgId: string): unknown; @@ -78,7 +81,7 @@ export interface NodeIKernelMsgService { refuseReceiveOnlineFileMsg(peer: Peer, MsgId: string): unknown; - resendMsg(...args: unknown[]): unknown; + resendMsg(peer: Peer, msgId: string): Promise; recallMsg(...args: unknown[]): unknown; @@ -132,7 +135,20 @@ export interface NodeIKernelMsgService { isMsgMatched(...args: unknown[]): unknown; - getOnlineFileMsgs(...args: unknown[]): unknown; + getOnlineFileMsgs(peer: Peer): Promise ; getAllOnlineFileMsgs(...args: unknown[]): unknown; @@ -418,11 +434,25 @@ export interface NodeIKernelMsgService { refreshMsgAbstractsByGuildIds(...args: unknown[]): unknown; - getRichMediaElement(...args: unknown[]): unknown; + getRichMediaElement(arg: { + msgId: string, + peerUid: string, + chatType: number, + elementId: string, + downSourceType: number, + downloadType: number, + }): Promise; cancelGetRichMediaElement(...args: unknown[]): unknown; - refuseGetRichMediaElement(...args: unknown[]): unknown; + refuseGetRichMediaElement(args: { + msgId: string, + peerUid: string, + chatType: number, + elementId: string, + downloadType: number, // 1 + downSourceType: number, // 1 + }): Promise; switchToOfflineGetRichMediaElement(...args: unknown[]): unknown; diff --git a/packages/napcat-core/services/common.ts b/packages/napcat-core/services/common.ts index 64ec0cb2..c7c8e0ab 100644 --- a/packages/napcat-core/services/common.ts +++ b/packages/napcat-core/services/common.ts @@ -1,5 +1,6 @@ export enum GeneralCallResultStatus { OK = 0, + ERROR = -1, } export interface GeneralCallResult { diff --git a/packages/napcat-core/types/flashfile.ts b/packages/napcat-core/types/flashfile.ts new file mode 100644 index 00000000..b057cfb7 --- /dev/null +++ b/packages/napcat-core/types/flashfile.ts @@ -0,0 +1,21 @@ +export enum fileType { + MP3 = 1, + VIDEO = 2, + DOC = 3, + ZIP = 4, + XLS = 6, + PPT = 7, + CODE = 8, + PDF = 9, + TXT = 10, + UNKNOW = 11, + FOLDER = 25, + IMG = 26, +} + +export enum FileStatus { + UPLOADING = 0, + // DOWNLOADED = 1, ??? 不太清楚 + OK = 2, + STOP = 3, +} diff --git a/packages/napcat-core/types/msg.ts b/packages/napcat-core/types/msg.ts index 6f6b57f3..a84121ee 100644 --- a/packages/napcat-core/types/msg.ts +++ b/packages/napcat-core/types/msg.ts @@ -66,13 +66,14 @@ export enum ElementType { YOLOGAMERESULT = 20, AVRECORD = 21, FEED = 22, - TOFURECORD = 23, + TOFURECORD = 23, // tofu record?? 在线文件的id是这个 ACEBUBBLE = 24, ACTIVITY = 25, TOFU = 26, FACEBUBBLE = 27, SHARELOCATION = 28, TASKTOPMSG = 29, + ONLINEFOLDER = 30, // 在线文件夹 RECOMMENDEDMSG = 43, ACTIONBAR = 44, } @@ -303,11 +304,40 @@ export enum NTVideoType { VIDEO_FORMAT_WMV = 3, } +/** + * 闪传图标 + */ +export interface FlashTransferIcon { + spec: number; + url: string; +} + +/** + * 闪传文件信息 + */ +export interface FlashTransferInfo { + filesetId: string; + name: string; + fileSize: string; + thnumbnail: { + id: string; + urls: FlashTransferIcon[]; + localCachePath: string; + } +} + /** * Markdown元素接口 */ export interface MarkdownElement { content: string; + style?: {}; + processMsg?: string; + mdSummary?: string; + mdExtType?: number; + mdExtInfo?: { + flashTransferInfo: FlashTransferInfo; + } } /** diff --git a/packages/napcat-core/wrapper.ts b/packages/napcat-core/wrapper.ts index 90cd3e08..c94bf42c 100644 --- a/packages/napcat-core/wrapper.ts +++ b/packages/napcat-core/wrapper.ts @@ -27,6 +27,7 @@ import { NodeIKernelMSFService } from './services/NodeIKernelMSFService'; import { NodeIkernelTestPerformanceService } from './services/NodeIkernelTestPerformanceService'; import { NodeIKernelECDHService } from './services/NodeIKernelECDHService'; import { NodeIO3MiscService } from './services/NodeIO3MiscService'; +import { NodeIKernelFlashTransferService } from "./services/NodeIKernelFlashTransferService"; export interface NodeQQNTWrapperUtil { get(): NodeQQNTWrapperUtil; @@ -202,6 +203,8 @@ export interface NodeIQQNTWrapperSession { getSearchService(): NodeIKernelSearchService; + getFlashTransferService(): NodeIKernelFlashTransferService; + getDirectSessionService(): unknown; getRDeliveryService(): unknown; diff --git a/packages/napcat-onebot/action/file/flash/CreateFlashTask.ts b/packages/napcat-onebot/action/file/flash/CreateFlashTask.ts new file mode 100644 index 00000000..7d2f811b --- /dev/null +++ b/packages/napcat-onebot/action/file/flash/CreateFlashTask.ts @@ -0,0 +1,24 @@ +import { OneBotAction } from '@/napcat-onebot/action/OneBotAction'; +import { ActionName } from '@/napcat-onebot/action/router'; +import { Static, Type } from '@sinclair/typebox'; + +// 不全部使用json因为:一个文件解析Form-data会变字符串!!! 但是api文档就写List +const SchemaData = Type.Object({ + files: Type.Union([ + Type.Array(Type.String()), + Type.String(), + ]), +}); +type Payload = Static; + +export class CreateFlashTask extends OneBotAction { + override actionName = ActionName.CreateFlashTask; + override payloadSchema = SchemaData; + + async _handle (payload: Payload) { + // todo fileset的名字和缩略图还没实现!! + const fileList = Array.isArray(payload.files) ? payload.files : [payload.files]; + + return await this.core.apis.FlashApi.createFlashTransferUploadTask(fileList); + } +} diff --git a/packages/napcat-onebot/action/file/flash/DownloadFileset.ts b/packages/napcat-onebot/action/file/flash/DownloadFileset.ts new file mode 100644 index 00000000..0cb84c9f --- /dev/null +++ b/packages/napcat-onebot/action/file/flash/DownloadFileset.ts @@ -0,0 +1,19 @@ +import { OneBotAction } from '@/napcat-onebot/action/OneBotAction'; +import { ActionName } from '@/napcat-onebot/action/router'; +import { Static, Type } from '@sinclair/typebox'; + +const SchemaData = Type.Object({ + fileset_id: Type.String(), +}); + +type Payload = Static; + +export class DownloadFileset extends OneBotAction { + override actionName = ActionName.DownloadFileset; + override payloadSchema = SchemaData; + + async _handle (payload: Payload) { + // 默认路径 / fileset_id /为下载路径 + return await this.core.apis.FlashApi.downloadFileSetBySetId(payload.fileset_id); + } +} diff --git a/packages/napcat-onebot/action/file/flash/GetFilesetIdByCode.ts b/packages/napcat-onebot/action/file/flash/GetFilesetIdByCode.ts new file mode 100644 index 00000000..1662c0c2 --- /dev/null +++ b/packages/napcat-onebot/action/file/flash/GetFilesetIdByCode.ts @@ -0,0 +1,20 @@ +import { OneBotAction } from '@/napcat-onebot/action/OneBotAction'; +import { ActionName } from '@/napcat-onebot/action/router'; +import { Static, Type } from '@sinclair/typebox'; + +const SchemaData = Type.Object({ + share_code: Type.String(), +}); + +type Payload = Static; + +export class GetFilesetId extends OneBotAction { + override actionName = ActionName.GetFilesetId; + override payloadSchema = SchemaData; + + async _handle (payload: Payload) { + // 适配share_link 防止被传 Link无法解析 + const code = payload.share_code.includes('=') ? payload.share_code.split('=').slice(1).join('=') : payload.share_code; + return await this.core.apis.FlashApi.fromShareLinkFindSetId(code); + } +} diff --git a/packages/napcat-onebot/action/file/flash/GetFilesetInfo.ts b/packages/napcat-onebot/action/file/flash/GetFilesetInfo.ts new file mode 100644 index 00000000..7fc77d0c --- /dev/null +++ b/packages/napcat-onebot/action/file/flash/GetFilesetInfo.ts @@ -0,0 +1,18 @@ +import { OneBotAction } from '@/napcat-onebot/action/OneBotAction'; +import { ActionName } from '@/napcat-onebot/action/router'; +import { Static, Type } from '@sinclair/typebox'; + +const SchemaData = Type.Object({ + fileset_id: Type.String(), +}); + +type Payload = Static; + +export class GetFilesetInfo extends OneBotAction { + override actionName = ActionName.GetFilesetInfo; + override payloadSchema = SchemaData; + + async _handle (payload: Payload) { + return await this.core.apis.FlashApi.getFileSetIndoBySetId(payload.fileset_id); + } +} diff --git a/packages/napcat-onebot/action/file/flash/GetFlashFileList.ts b/packages/napcat-onebot/action/file/flash/GetFlashFileList.ts new file mode 100644 index 00000000..e8ee1ea9 --- /dev/null +++ b/packages/napcat-onebot/action/file/flash/GetFlashFileList.ts @@ -0,0 +1,18 @@ +import { OneBotAction } from '@/napcat-onebot/action/OneBotAction'; +import { ActionName } from '@/napcat-onebot/action/router'; +import { Static, Type } from '@sinclair/typebox'; + +const SchemaData = Type.Object({ + fileset_id: Type.String(), +}); + +type Payload = Static; + +export class GetFlashFileList extends OneBotAction { + override actionName = ActionName.GetFlashFileList; + override payloadSchema = SchemaData; + + async _handle (payload: Payload) { + return await this.core.apis.FlashApi.getFileListBySetId(payload.fileset_id); + } +} diff --git a/packages/napcat-onebot/action/file/flash/GetFlashFileUrl.ts b/packages/napcat-onebot/action/file/flash/GetFlashFileUrl.ts new file mode 100644 index 00000000..403ad02f --- /dev/null +++ b/packages/napcat-onebot/action/file/flash/GetFlashFileUrl.ts @@ -0,0 +1,24 @@ +import { OneBotAction } from '@/napcat-onebot/action/OneBotAction'; +import { ActionName } from '@/napcat-onebot/action/router'; +import { Static, Type } from '@sinclair/typebox'; + +const SchemaData = Type.Object({ + fileset_id: Type.String(), + file_name: Type.Optional(Type.String()), + file_index: Type.Optional(Type.Number()), +}); + +type Payload = Static; + +export class GetFlashFileUrl extends OneBotAction { + override actionName = ActionName.GetFlashFileUrl; + override payloadSchema = SchemaData; + + async _handle (payload: Payload) { + // 文件的索引依旧从0开始 + return await this.core.apis.FlashApi.getFileTransUrl(payload.fileset_id, { + fileName: payload.file_name, + fileIndex: payload.file_index, + }); + } +} diff --git a/packages/napcat-onebot/action/file/flash/GetShareLink.ts b/packages/napcat-onebot/action/file/flash/GetShareLink.ts new file mode 100644 index 00000000..ea749cbd --- /dev/null +++ b/packages/napcat-onebot/action/file/flash/GetShareLink.ts @@ -0,0 +1,18 @@ +import { OneBotAction } from '@/napcat-onebot/action/OneBotAction'; +import { ActionName } from '@/napcat-onebot/action/router'; +import { Static, Type } from '@sinclair/typebox'; + +const SchemaData = Type.Object({ + fileset_id: Type.String(), +}); + +type Payload = Static; + +export class GetShareLink extends OneBotAction { + override actionName = ActionName.GetShareLink; + override payloadSchema = SchemaData; + + async _handle (payload: Payload) { + return await this.core.apis.FlashApi.getShareLinkBySetId(payload.fileset_id); + } +} diff --git a/packages/napcat-onebot/action/file/flash/SendFlashMsg.ts b/packages/napcat-onebot/action/file/flash/SendFlashMsg.ts new file mode 100644 index 00000000..abc71cba --- /dev/null +++ b/packages/napcat-onebot/action/file/flash/SendFlashMsg.ts @@ -0,0 +1,39 @@ +import { OneBotAction } from '@/napcat-onebot/action/OneBotAction'; +import { ActionName } from '@/napcat-onebot/action/router'; +import { Static, Type } from '@sinclair/typebox'; +import { ChatType, Peer } from 'napcat-core/types'; + +const SchemaData = Type.Object({ + fileset_id: Type.String(), + user_id: Type.Optional(Type.Union([Type.Number(), Type.String()])), + group_id: Type.Optional(Type.Union([Type.Number(), Type.String()])), +}); + +type Payload = Static; + +export class SendFlashMsg extends OneBotAction { + override actionName = ActionName.SendFlashMsg; + override payloadSchema = SchemaData; + + async _handle (payload: Payload) { + let peer: Peer; + + if (payload.group_id) { + peer = { chatType: ChatType.KCHATTYPEGROUP, peerUid: payload.group_id.toString() }; + } else if (payload.user_id) { + const uid = await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString()); + if (!uid) throw new Error('User not found'); + + // 可能需要更严格的判断 + const isBuddy = await this.core.apis.FriendApi.isBuddy(uid); + peer = { + chatType: isBuddy ? ChatType.KCHATTYPEC2C : ChatType.KCHATTYPETEMPC2CFROMGROUP, + peerUid: uid, + }; + } else { + throw new Error('user_id or group_id is required'); + } + + return await this.core.apis.FlashApi.sendFlashMessage(payload.fileset_id, peer); + } +} diff --git a/packages/napcat-onebot/action/file/online/CancelOnlineFile.ts b/packages/napcat-onebot/action/file/online/CancelOnlineFile.ts new file mode 100644 index 00000000..384798f0 --- /dev/null +++ b/packages/napcat-onebot/action/file/online/CancelOnlineFile.ts @@ -0,0 +1,26 @@ +import { OneBotAction } from '@/napcat-onebot/action/OneBotAction'; +import { ActionName } from '@/napcat-onebot/action/router'; +import { Static, Type } from '@sinclair/typebox'; +import { ChatType } from 'napcat-core/types'; + +const SchemaData = Type.Object({ + user_id: Type.Union([Type.Number(), Type.String()]), + msg_id: Type.String(), +}); + +type Payload = Static; + +export class CancelOnlineFile extends OneBotAction { + override actionName = ActionName.CancelOnlineFile; + override payloadSchema = SchemaData; + + async _handle (payload: Payload) { + const uid = await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString()); + if (!uid) throw new Error('User not found'); + + // 仅私聊 + const peer = { chatType: ChatType.KCHATTYPEC2C, peerUid: uid }; + + return await this.core.apis.OnlineApi.cancelMyOnlineFileMsg(peer, payload.msg_id); + } +} diff --git a/packages/napcat-onebot/action/file/online/GetOnlineFileMessages.ts b/packages/napcat-onebot/action/file/online/GetOnlineFileMessages.ts new file mode 100644 index 00000000..0cd0a320 --- /dev/null +++ b/packages/napcat-onebot/action/file/online/GetOnlineFileMessages.ts @@ -0,0 +1,25 @@ +import { OneBotAction } from '@/napcat-onebot/action/OneBotAction'; +import { ActionName } from '@/napcat-onebot/action/router'; +import { Static, Type } from '@sinclair/typebox'; +import { ChatType } from 'napcat-core/types'; + +const SchemaData = Type.Object({ + user_id: Type.Union([Type.Number(), Type.String()]), +}); + +type Payload = Static; + +export class GetOnlineFileMessages extends OneBotAction { + override actionName = ActionName.GetOnlineFileMessages; + override payloadSchema = SchemaData; + + async _handle (payload: Payload) { + const uid = await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString()); + if (!uid) throw new Error('User not found'); + + // 仅私聊 + const peer = { chatType: ChatType.KCHATTYPEC2C, peerUid: uid }; + + return await this.core.apis.OnlineApi.getOnlineFileMsg(peer); + } +} diff --git a/packages/napcat-onebot/action/file/online/ReceiveOnlineFile.ts b/packages/napcat-onebot/action/file/online/ReceiveOnlineFile.ts new file mode 100644 index 00000000..5204d1d1 --- /dev/null +++ b/packages/napcat-onebot/action/file/online/ReceiveOnlineFile.ts @@ -0,0 +1,27 @@ +import { OneBotAction } from '@/napcat-onebot/action/OneBotAction'; +import { ActionName } from '@/napcat-onebot/action/router'; +import { Static, Type } from '@sinclair/typebox'; +import { ChatType } from 'napcat-core/types'; + +const SchemaData = Type.Object({ + user_id: Type.Union([Type.Number(), Type.String()]), + msg_id: Type.String(), + element_id: Type.String(), +}); + +type Payload = Static; + +export class ReceiveOnlineFile extends OneBotAction { + override actionName = ActionName.ReceiveOnlineFile; + override payloadSchema = SchemaData; + + async _handle (payload: Payload) { + // 默认下载路径 + const uid = await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString()); + if (!uid) throw new Error('User not found'); + + const peer = { chatType: ChatType.KCHATTYPEC2C, peerUid: uid }; + + return await this.core.apis.OnlineApi.receiveOnlineFileOrFolder(peer, payload.msg_id, payload.element_id); + } +} diff --git a/packages/napcat-onebot/action/file/online/RefuseOnlineFile.ts b/packages/napcat-onebot/action/file/online/RefuseOnlineFile.ts new file mode 100644 index 00000000..6b3ff972 --- /dev/null +++ b/packages/napcat-onebot/action/file/online/RefuseOnlineFile.ts @@ -0,0 +1,26 @@ +import { OneBotAction } from '@/napcat-onebot/action/OneBotAction'; +import { ActionName } from '@/napcat-onebot/action/router'; +import { Static, Type } from '@sinclair/typebox'; +import { ChatType } from 'napcat-core/types'; + +const SchemaData = Type.Object({ + user_id: Type.Union([Type.Number(), Type.String()]), + msg_id: Type.String(), + element_id: Type.String(), +}); + +type Payload = Static; + +export class RefuseOnlineFile extends OneBotAction { + override actionName = ActionName.RefuseOnlineFile; + override payloadSchema = SchemaData; + + async _handle (payload: Payload) { + const uid = await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString()); + if (!uid) throw new Error('User not found'); + + const peer = { chatType: ChatType.KCHATTYPEC2C, peerUid: uid }; + + return await this.core.apis.OnlineApi.refuseOnlineFileMsg(peer, payload.msg_id, payload.element_id); + } +} diff --git a/packages/napcat-onebot/action/file/online/SendOnlineFile.ts b/packages/napcat-onebot/action/file/online/SendOnlineFile.ts new file mode 100644 index 00000000..b5a657a3 --- /dev/null +++ b/packages/napcat-onebot/action/file/online/SendOnlineFile.ts @@ -0,0 +1,28 @@ +import { OneBotAction } from '@/napcat-onebot/action/OneBotAction'; +import { ActionName } from '@/napcat-onebot/action/router'; +import { Static, Type } from '@sinclair/typebox'; +import { ChatType } from 'napcat-core/types'; + +const SchemaData = Type.Object({ + user_id: Type.Union([Type.Number(), Type.String()]), + file_path: Type.String(), + file_name: Type.Optional(Type.String()), +}); + +type Payload = Static; + +export class SendOnlineFile extends OneBotAction { + override actionName = ActionName.SendOnlineFile; + override payloadSchema = SchemaData; + + async _handle (payload: Payload) { + const uid = await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString()); + if (!uid) throw new Error('User not found'); + + // 仅私聊 + const peer = { chatType: ChatType.KCHATTYPEC2C, peerUid: uid }; + const fileName = payload.file_name || ''; + + return await this.core.apis.OnlineApi.sendOnlineFile(peer, payload.file_path, fileName); + } +} diff --git a/packages/napcat-onebot/action/file/online/SendOnlineFolder.ts b/packages/napcat-onebot/action/file/online/SendOnlineFolder.ts new file mode 100644 index 00000000..a4da4855 --- /dev/null +++ b/packages/napcat-onebot/action/file/online/SendOnlineFolder.ts @@ -0,0 +1,26 @@ +import { OneBotAction } from '@/napcat-onebot/action/OneBotAction'; +import { ActionName } from '@/napcat-onebot/action/router'; +import { Static, Type } from '@sinclair/typebox'; +import { ChatType } from 'napcat-core/types'; + +const SchemaData = Type.Object({ + user_id: Type.Union([Type.Number(), Type.String()]), + folder_path: Type.String(), + folder_name: Type.Optional(Type.String()), +}); + +type Payload = Static; + +export class SendOnlineFolder extends OneBotAction { + override actionName = ActionName.SendOnlineFolder; + override payloadSchema = SchemaData; + + async _handle (payload: Payload) { + const uid = await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString()); + if (!uid) throw new Error('User not found'); + + const peer = { chatType: ChatType.KCHATTYPEC2C, peerUid: uid }; + + return await this.core.apis.OnlineApi.sendOnlineFolder(peer, payload.folder_path, payload.folder_name); + } +} diff --git a/packages/napcat-onebot/action/index.ts b/packages/napcat-onebot/action/index.ts index a8ba3485..1903a694 100644 --- a/packages/napcat-onebot/action/index.ts +++ b/packages/napcat-onebot/action/index.ts @@ -66,9 +66,9 @@ import { FetchCustomFace } from './extends/FetchCustomFace'; import GoCQHTTPUploadPrivateFile from './go-cqhttp/UploadPrivateFile'; import { FetchEmojiLike } from './extends/FetchEmojiLike'; import { NapCatCore } from 'napcat-core'; -import { NapCatOneBot11Adapter } from '@/napcat-onebot/index'; import type { NetworkAdapterConfig } from '../config/config'; import { OneBotAction } from './OneBotAction'; +import { NapCatOneBot11Adapter } from '@/napcat-onebot'; import { SetInputStatus } from './extends/SetInputStatus'; import { GetCSRF } from './system/GetCSRF'; import { DelGroupNotice } from './group/DelGroupNotice'; @@ -140,6 +140,20 @@ import { DownloadFileImageStream } from './stream/DownloadFileImageStream'; import { TestDownloadStream } from './stream/TestStreamDownload'; import { UploadFileStream } from './stream/UploadFileStream'; import { AutoRegisterRouter } from './auto-register'; +import { CreateFlashTask } from './file/flash/CreateFlashTask'; +import { SendFlashMsg } from './file/flash/SendFlashMsg'; +import { GetFlashFileList } from './file/flash/GetFlashFileList'; +import { GetFlashFileUrl } from './file/flash/GetFlashFileUrl'; +import { GetShareLink } from './file/flash/GetShareLink'; +import { GetFilesetInfo } from './file/flash/GetFilesetInfo'; +import { DownloadFileset } from './file/flash/DownloadFileset'; +import { GetOnlineFileMessages } from './file/online/GetOnlineFileMessages'; +import { SendOnlineFile } from './file/online/SendOnlineFile'; +import { SendOnlineFolder } from './file/online/SendOnlineFolder'; +import { CancelOnlineFile } from './file/online/CancelOnlineFile'; +import { ReceiveOnlineFile } from './file/online/ReceiveOnlineFile'; +import { RefuseOnlineFile } from './file/online/RefuseOnlineFile'; +import { GetFilesetId } from './file/flash/GetFilesetIdByCode'; export function createActionMap (obContext: NapCatOneBot11Adapter, core: NapCatCore) { const actionHandlers = [ @@ -293,6 +307,20 @@ export function createActionMap (obContext: NapCatOneBot11Adapter, core: NapCatC new CleanCache(obContext, core), new GetGroupAddRequest(obContext, core), new GetCollectionList(obContext, core), + new CreateFlashTask(obContext, core), + new GetFlashFileList(obContext, core), + new GetFlashFileUrl(obContext, core), + new SendFlashMsg(obContext, core), + new GetShareLink(obContext, core), + new GetFilesetInfo(obContext, core), + new GetOnlineFileMessages(obContext, core), + new SendOnlineFile(obContext, core), + new SendOnlineFolder(obContext, core), + new ReceiveOnlineFile(obContext, core), + new RefuseOnlineFile(obContext, core), + new CancelOnlineFile(obContext, core), + new DownloadFileset(obContext, core), + new GetFilesetId(obContext, core), ]; type HandlerUnion = typeof actionHandlers[number]; diff --git a/packages/napcat-onebot/action/router.ts b/packages/napcat-onebot/action/router.ts index 9fb2ff14..1428b17c 100644 --- a/packages/napcat-onebot/action/router.ts +++ b/packages/napcat-onebot/action/router.ts @@ -125,8 +125,8 @@ export const ActionName = { // 以下为扩展napcat扩展 Unknown: 'unknown', SetDiyOnlineStatus: 'set_diy_online_status', - SharePeer: 'ArkSharePeer',// @deprecated - ShareGroupEx: 'ArkShareGroup',// @deprecated + SharePeer: 'ArkSharePeer', // @deprecated + ShareGroupEx: 'ArkShareGroup', // @deprecated // 标准化接口 SendGroupArkShare: 'send_group_ark_share', SendArkShare: 'send_ark_share', @@ -185,4 +185,22 @@ export const ActionName = { GetClientkey: 'get_clientkey', SendPoke: 'send_poke', + + // Flash (闪传) 扩展 + CreateFlashTask: 'create_flash_task', + SendFlashMsg: 'send_flash_msg', // 因为不可能手动构造element,所以不走sendMsg + GetShareLink: 'get_share_link', + DownloadFileset: 'download_fileset', + GetFilesetInfo: 'get_fileset_info', + GetFlashFileList: 'get_flash_file_list', + GetFlashFileUrl: 'get_flash_file_url', + GetFilesetId: 'get_fileset_id', + + // Online File (在线文件) 扩展 + SendOnlineFile: 'send_online_file', + SendOnlineFolder: 'send_online_folder', + GetOnlineFileMessages: 'get_online_file_msg', + ReceiveOnlineFile: 'receive_online_file', + RefuseOnlineFile: 'refuse_online_file', + CancelOnlineFile: 'cancel_online_file', } as const; diff --git a/packages/napcat-onebot/api/msg.ts b/packages/napcat-onebot/api/msg.ts index 6f267d42..77cde524 100644 --- a/packages/napcat-onebot/api/msg.ts +++ b/packages/napcat-onebot/api/msg.ts @@ -42,11 +42,18 @@ import { OB11GroupIncreaseEvent } from '../event/notice/OB11GroupIncreaseEvent'; import { GroupDecreaseSubType, OB11GroupDecreaseEvent } from '../event/notice/OB11GroupDecreaseEvent'; import { GroupAdmin } from 'napcat-core/packet/transformer/proto/message/groupAdmin'; import { OB11GroupAdminNoticeEvent } from '../event/notice/OB11GroupAdminNoticeEvent'; -import { GroupChange, GroupChangeInfo, GroupInvite, PushMsgBody } from 'napcat-core/packet/transformer/proto'; +import { + GroupChange, + GroupChangeInfo, + GroupInvite, + PushMsgBody, +} from 'napcat-core/packet/transformer/proto'; import { OB11GroupRequestEvent } from '../event/request/OB11GroupRequest'; import { LRUCache } from 'napcat-common/src/lru-cache'; import { cleanTaskQueue } from 'napcat-common/src/clean-task'; import { registerResource } from 'napcat-common/src/health'; +import { OB11OnlineFileReceiveEvent } from '@/napcat-onebot/event/notice/OB11OnlineFileReceiveEvent'; +import { OB11OnlineFileSendEvent } from '@/napcat-onebot/event/notice/OB11OnlineFileSendEvent'; type RawToOb11Converters = { [Key in keyof MessageElement as Key extends `${string}Element` ? Key : never]: ( @@ -143,6 +150,21 @@ export class OneBotMsgApi { }, fileElement: async (element, msg, elementWrapper, { disableGetUrl }) => { + // 让在线文件/文件夹的消息单独出去(否则无法正确处理UUID!!!) + if (+elementWrapper.elementType === 23 || +elementWrapper.elementType === 30) { + // 判断为在线文件/文件夹 + return { + type: OB11MessageDataType.onlinefile, + data: { + msgId: msg.msgId, + elementId: elementWrapper.elementId, + fileName: element.fileName, + fileSize: element.fileSize, + isDir: (elementWrapper.elementType === 30), + }, + }; + } + const peer = { chatType: msg.chatType, peerUid: msg.peerUid, @@ -538,12 +560,22 @@ export class OneBotMsgApi { }, markdownElement: async (element) => { - return { - type: OB11MessageDataType.markdown, - data: { - content: element.content, - }, - }; + // 让QQ闪传消息独立出去 + if (element.mdExtInfo !== undefined && element.mdExtInfo.flashTransferInfo) { + return { + type: OB11MessageDataType.flashtransfer, + data: { + fileSetId: element.mdExtInfo.flashTransferInfo.filesetId, + }, + }; + } else { + return { + type: OB11MessageDataType.markdown, + data: { + content: element.content, + }, + }; + } }, }; @@ -880,6 +912,10 @@ export class OneBotMsgApi { } return undefined; }, + // 不需要支持发送 + [OB11MessageDataType.onlinefile]: async () => undefined, + + [OB11MessageDataType.flashtransfer]: async () => undefined, }; constructor (obContext: NapCatOneBot11Adapter, core: NapCatCore) { @@ -1329,6 +1365,7 @@ export class OneBotMsgApi { async parseSysMessage (msg: number[]) { const SysMessage = new NapProtoMsg(PushMsgBody).decode(Uint8Array.from(msg)); // 邀请需要解grayTipElement + // console.log(SysMessage.body?.msgContent); if (SysMessage.contentHead.type === 33 && SysMessage.body?.msgContent) { const groupChange = new NapProtoMsg(GroupChange).decode(SysMessage.body.msgContent); await this.core.apis.GroupApi.refreshGroupMemberCache(groupChange.groupUin.toString(), true); @@ -1484,6 +1521,63 @@ export class OneBotMsgApi { ); } else if (SysMessage.contentHead.type === 528 && SysMessage.contentHead.subType === 39 && SysMessage.body?.msgContent) { return await this.obContext.apis.UserApi.parseLikeEvent(SysMessage.body?.msgContent); + } else if (SysMessage.contentHead.type === 166 && SysMessage.contentHead.c2CCmd === 133 && SysMessage.body?.msgContent) { + this.core.context.logger.logDebug('在线文件通道断开'); + // 可能原因: 对方取消 对方拒绝 对方转离线 + // body不是proto,只能手动提取,可能是错的!! + // console.log(SysMessage.body?.msgContent); + const mainCmd = SysMessage.body.msgContent[15]; + const subCmd = SysMessage.body.msgContent[17]; + if (mainCmd === 101) { + // 在线文件 + if (subCmd === 225) { + // 对方取消或转离线 + this.core.context.logger.log(`好友:${SysMessage.responseHead.fromUin}取消了在线文件的传输(或转离线)`); + return new OB11OnlineFileReceiveEvent( + this.core, + +SysMessage.responseHead.fromUin + ); + } else if (subCmd === 230) { + // 对方拒绝接收 + this.core.context.logger.log(`好友:${SysMessage.responseHead.fromUin}拒绝了你的在线文件传输`); + return new OB11OnlineFileSendEvent( + this.core, + +SysMessage.responseHead.fromUin, + 'refuse' + ); + } + } else if (mainCmd === 136) { + if (subCmd === 225) { + // 对方取消或转离线 + this.core.context.logger.log(`好友:${SysMessage.responseHead.fromUin}取消了在线文件夹的传输(或转离线)`); + return new OB11OnlineFileReceiveEvent( + this.core, + +SysMessage.responseHead.fromUin + ); + } else if (subCmd === 230) { + // 对方拒绝接收 + this.core.context.logger.log(`好友:${SysMessage.responseHead.fromUin}拒绝了你的在线文件夹传输`); + return new OB11OnlineFileSendEvent( + this.core, + +SysMessage.responseHead.fromUin, + 'refuse' + ); + } + } + this.core.context.logger.logDebug('未知的系统消息事件:', mainCmd, subCmd); + return undefined; + } else if (SysMessage.contentHead.type === 166 && SysMessage.contentHead.c2CCmd === 131 && SysMessage.body?.msgContent) { + const mainCmd = SysMessage.body.msgContent[15]; + if (mainCmd === 101) { + this.core.context.logger.log('在线文件传输成功!'); + } else if (mainCmd === 136) { + this.core.context.logger.log('在线文件夹传输成功!'); + } + return new OB11OnlineFileSendEvent( + this.core, + +SysMessage.responseHead.fromUin, + 'receive' + ); } // else if (SysMessage.contentHead.type == 732 && SysMessage.contentHead.subType == 16 && SysMessage.body?.msgContent) { // let data_wrap = PBString(2); diff --git a/packages/napcat-onebot/event/notice/OB11OnlineFileNoticeEvent.ts b/packages/napcat-onebot/event/notice/OB11OnlineFileNoticeEvent.ts new file mode 100644 index 00000000..375c940f --- /dev/null +++ b/packages/napcat-onebot/event/notice/OB11OnlineFileNoticeEvent.ts @@ -0,0 +1,11 @@ +import { OB11BaseNoticeEvent } from './OB11BaseNoticeEvent'; +import { NapCatCore } from 'napcat-core'; + +export abstract class OB11OnlineFileNoticeEvent extends OB11BaseNoticeEvent { + peer_id: number; + + protected constructor (core: NapCatCore, peer_id: number) { + super(core); + this.peer_id = peer_id; + } +} diff --git a/packages/napcat-onebot/event/notice/OB11OnlineFileReceiveEvent.ts b/packages/napcat-onebot/event/notice/OB11OnlineFileReceiveEvent.ts new file mode 100644 index 00000000..674b8efa --- /dev/null +++ b/packages/napcat-onebot/event/notice/OB11OnlineFileReceiveEvent.ts @@ -0,0 +1,13 @@ +import { OB11OnlineFileNoticeEvent } from './OB11OnlineFileNoticeEvent'; +import { NapCatCore } from '@/napcat-core'; + +export class OB11OnlineFileReceiveEvent extends OB11OnlineFileNoticeEvent { + notice_type: string; + sub_type: string; + + constructor (core: NapCatCore, peer_id: number) { + super(core, peer_id); + this.notice_type = 'online_file_receive'; + this.sub_type = 'cancel'; + } +} diff --git a/packages/napcat-onebot/event/notice/OB11OnlineFileSendEvent.ts b/packages/napcat-onebot/event/notice/OB11OnlineFileSendEvent.ts new file mode 100644 index 00000000..0f19fc0d --- /dev/null +++ b/packages/napcat-onebot/event/notice/OB11OnlineFileSendEvent.ts @@ -0,0 +1,12 @@ +import { OB11OnlineFileNoticeEvent } from './OB11OnlineFileNoticeEvent'; +import { NapCatCore } from '@/napcat-core'; + +export class OB11OnlineFileSendEvent extends OB11OnlineFileNoticeEvent { + notice_type = 'online_file_send'; + sub_type: 'receive' | 'refuse'; + + constructor (core: NapCatCore, peer_id: number, sub_type: 'receive' | 'refuse') { + super(core, peer_id); + this.sub_type = sub_type; + } +} diff --git a/packages/napcat-onebot/index.ts b/packages/napcat-onebot/index.ts index d4aee8e6..af377980 100644 --- a/packages/napcat-onebot/index.ts +++ b/packages/napcat-onebot/index.ts @@ -328,6 +328,38 @@ export class NapCatOneBot11Adapter { ); } }; + + /** + * 加入在线文件的listener + */ + msgListener.onRecvOnlineFileMsg = async (msg: RawMessage[]) => { + if (!this.networkManager.hasActiveAdapters()) { + return; + } + + for (const m of msg) { + // this.context.logger.logMessage(m, this.core.selfInfo); + + if (this.bootTime > parseInt(m.msgTime)) { + this.context.logger.logDebug(`在线文件消息时间${m.msgTime}早于启动时间${this.bootTime},忽略上报`); + continue; + } + + m.id = MessageUnique.createUniqueMsgId( + { + chatType: m.chatType, + peerUid: m.peerUid, + guildId: '', + }, + m.msgId + ); + + await this.emitMsg(m).catch((e) => + this.context.logger.logError('处理在线文件消息失败', e) + ); + } + }; + msgListener.onAddSendMsg = async (msg) => { try { if (msg.sendStatus === SendStatusType.KSEND_STATUS_SENDING) { diff --git a/packages/napcat-onebot/types/message.ts b/packages/napcat-onebot/types/message.ts index 6b8707de..bf566b81 100644 --- a/packages/napcat-onebot/types/message.ts +++ b/packages/napcat-onebot/types/message.ts @@ -73,6 +73,8 @@ export enum OB11MessageDataType { miniapp = 'miniapp', // json类 contact = 'contact', location = 'location', + onlinefile = 'onlinefile', // 在线文件/文件夹 + flashtransfer = 'flashtransfer', // QQ闪传 } export interface OB11MessagePoke { @@ -254,6 +256,24 @@ export interface OB11MessageForward { }; } +export interface OB11MessageOnlineFile { + type: OB11MessageDataType.onlinefile; + data: { + msgId: string; + elementId: string; + fileName: string; + fileSize: string; + isDir: boolean; + } +} + +export interface OB11MessageFlashTransfer { + type: OB11MessageDataType.flashtransfer; + data: { + fileSetId: string; + } +} + // 消息数据类型定义 export type OB11MessageData = OB11MessageText | @@ -261,7 +281,8 @@ export type OB11MessageData = OB11MessageAt | OB11MessageReply | OB11MessageImage | OB11MessageRecord | OB11MessageFile | OB11MessageVideo | OB11MessageNode | OB11MessageIdMusic | OB11MessageCustomMusic | OB11MessageJson | - OB11MessageDice | OB11MessageRPS | OB11MessageMarkdown | OB11MessageForward | OB11MessageContact | OB11MessagePoke; + OB11MessageDice | OB11MessageRPS | OB11MessageMarkdown | OB11MessageForward | OB11MessageContact | + OB11MessagePoke | OB11MessageOnlineFile | OB11MessageFlashTransfer; // 发送消息接口定义 export interface OB11PostSendMsg {