mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2025-07-19 12:03:37 +00:00
Merge remote-tracking branch 'origin/v2' into v2
This commit is contained in:
301
src/common/utils/file.ts
Normal file
301
src/common/utils/file.ts
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import fsPromise, { stat } from 'fs/promises';
|
||||||
|
import crypto from 'crypto';
|
||||||
|
import util from 'util';
|
||||||
|
import path from 'node:path';
|
||||||
|
import * as fileType from 'file-type';
|
||||||
|
import { randomUUID } from 'crypto';
|
||||||
|
import { LogWrapper } from './log';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export function isGIF(path: string) {
|
||||||
|
const buffer = Buffer.alloc(4);
|
||||||
|
const fd = fs.openSync(path, 'r');
|
||||||
|
fs.readSync(fd, buffer, 0, 4, 0);
|
||||||
|
fs.closeSync(fd);
|
||||||
|
return buffer.toString() === 'GIF8';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义一个异步函数来检查文件是否存在
|
||||||
|
export function checkFileReceived(path: string, timeout: number = 3000): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
function check() {
|
||||||
|
if (fs.existsSync(path)) {
|
||||||
|
resolve();
|
||||||
|
} else if (Date.now() - startTime > timeout) {
|
||||||
|
reject(new Error(`文件不存在: ${path}`));
|
||||||
|
} else {
|
||||||
|
setTimeout(check, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
check();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 定义一个异步函数来检查文件是否存在
|
||||||
|
export async function checkFileReceived2(path: string, timeout: number = 3000): Promise<void> {
|
||||||
|
// 使用 Promise.race 来同时进行文件状态检查和超时计时
|
||||||
|
// Promise.race 会返回第一个解决(resolve)或拒绝(reject)的 Promise
|
||||||
|
await Promise.race([
|
||||||
|
checkFile(path),
|
||||||
|
timeoutPromise(timeout, `文件不存在: ${path}`),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换超时时间至 Promise
|
||||||
|
function timeoutPromise(timeout: number, errorMsg: string): Promise<void> {
|
||||||
|
return new Promise((_, reject) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
reject(new Error(errorMsg));
|
||||||
|
}, timeout);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 异步检查文件是否存在
|
||||||
|
async function checkFile(path: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
await stat(path);
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.code === 'ENOENT') {
|
||||||
|
// 如果文件不存在,则抛出一个错误
|
||||||
|
throw new Error(`文件不存在: ${path}`);
|
||||||
|
} else {
|
||||||
|
// 对于 stat 调用的其他错误,重新抛出
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 如果文件存在,则无需做任何事情,Promise 解决(resolve)自身
|
||||||
|
}
|
||||||
|
export async function file2base64(path: string) {
|
||||||
|
const readFile = util.promisify(fs.readFile);
|
||||||
|
const result = {
|
||||||
|
err: '',
|
||||||
|
data: ''
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
// 读取文件内容
|
||||||
|
// if (!fs.existsSync(path)){
|
||||||
|
// path = path.replace("\\Ori\\", "\\Thumb\\");
|
||||||
|
// }
|
||||||
|
try {
|
||||||
|
await checkFileReceived(path, 5000);
|
||||||
|
} catch (e: any) {
|
||||||
|
result.err = e.toString();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
const data = await readFile(path);
|
||||||
|
// 转换为Base64编码
|
||||||
|
result.data = data.toString('base64');
|
||||||
|
} catch (err: any) {
|
||||||
|
result.err = err.toString();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function calculateFileMD5(filePath: string): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// 创建一个流式读取器
|
||||||
|
const stream = fs.createReadStream(filePath);
|
||||||
|
const hash = crypto.createHash('md5');
|
||||||
|
|
||||||
|
stream.on('data', (data: Buffer) => {
|
||||||
|
// 当读取到数据时,更新哈希对象的状态
|
||||||
|
hash.update(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on('end', () => {
|
||||||
|
// 文件读取完成,计算哈希
|
||||||
|
const md5 = hash.digest('hex');
|
||||||
|
resolve(md5);
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on('error', (err: Error) => {
|
||||||
|
// 处理可能的读取错误
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HttpDownloadOptions {
|
||||||
|
url: string;
|
||||||
|
headers?: Record<string, string> | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function httpDownload(options: string | HttpDownloadOptions): Promise<Buffer> {
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
let url: string;
|
||||||
|
let headers: Record<string, string> = {
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36'
|
||||||
|
};
|
||||||
|
if (typeof options === 'string') {
|
||||||
|
url = options;
|
||||||
|
const host = new URL(url).hostname;
|
||||||
|
headers['Host'] = host;
|
||||||
|
} else {
|
||||||
|
url = options.url;
|
||||||
|
if (options.headers) {
|
||||||
|
if (typeof options.headers === 'string') {
|
||||||
|
headers = JSON.parse(options.headers);
|
||||||
|
} else {
|
||||||
|
headers = options.headers;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const fetchRes = await fetch(url, { headers }).catch((err) => {
|
||||||
|
if (err.cause) {
|
||||||
|
throw err.cause;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
if (!fetchRes.ok) throw new Error(`下载文件失败: ${fetchRes.statusText}`);
|
||||||
|
|
||||||
|
const blob = await fetchRes.blob();
|
||||||
|
const buffer = await blob.arrayBuffer();
|
||||||
|
return Buffer.from(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
type Uri2LocalRes = {
|
||||||
|
success: boolean,
|
||||||
|
errMsg: string,
|
||||||
|
fileName: string,
|
||||||
|
ext: string,
|
||||||
|
path: string,
|
||||||
|
isLocal: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function uri2local(TempDir: string, UriOrPath: string, fileName: string | null = null): Promise<Uri2LocalRes> {
|
||||||
|
const res = {
|
||||||
|
success: false,
|
||||||
|
errMsg: '',
|
||||||
|
fileName: '',
|
||||||
|
ext: '',
|
||||||
|
path: '',
|
||||||
|
isLocal: false
|
||||||
|
};
|
||||||
|
if (!fileName) fileName = randomUUID();
|
||||||
|
let filePath = path.join(TempDir, fileName);//临时目录
|
||||||
|
let url = null;
|
||||||
|
//区分path和uri
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(UriOrPath)) url = new URL('file://' + UriOrPath);
|
||||||
|
} catch (error: any) { }
|
||||||
|
try {
|
||||||
|
url = new URL(UriOrPath);
|
||||||
|
} catch (error: any) { }
|
||||||
|
|
||||||
|
//验证url
|
||||||
|
if (!url) {
|
||||||
|
res.errMsg = `UriOrPath ${UriOrPath} 解析失败,可能${UriOrPath}不存在`;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.protocol == 'base64:') {
|
||||||
|
// base64转成文件
|
||||||
|
const base64Data = UriOrPath.split('base64://')[1];
|
||||||
|
try {
|
||||||
|
const buffer = Buffer.from(base64Data, 'base64');
|
||||||
|
fs.writeFileSync(filePath, buffer);
|
||||||
|
} catch (e: any) {
|
||||||
|
res.errMsg = 'base64文件下载失败,' + e.toString();
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
} else if (url.protocol == 'http:' || url.protocol == 'https:') {
|
||||||
|
// 下载文件
|
||||||
|
let buffer: Buffer | null = null;
|
||||||
|
try {
|
||||||
|
buffer = await httpDownload(UriOrPath);
|
||||||
|
} catch (e: any) {
|
||||||
|
res.errMsg = `${url}下载失败,` + e.toString();
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const pathInfo = path.parse(decodeURIComponent(url.pathname));
|
||||||
|
if (pathInfo.name) {
|
||||||
|
fileName = pathInfo.name;
|
||||||
|
if (pathInfo.ext) {
|
||||||
|
fileName += pathInfo.ext;
|
||||||
|
// res.ext = pathInfo.ext
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fileName = fileName.replace(/[/\\:*?"<>|]/g, '_');
|
||||||
|
res.fileName = fileName;
|
||||||
|
filePath = path.join(TempDir, randomUUID() + fileName);
|
||||||
|
fs.writeFileSync(filePath, buffer);
|
||||||
|
} catch (e: any) {
|
||||||
|
res.errMsg = `${url}下载失败,` + e.toString();
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let pathname: string;
|
||||||
|
if (url.protocol === 'file:') {
|
||||||
|
// await fs.copyFile(url.pathname, filePath);
|
||||||
|
pathname = decodeURIComponent(url.pathname);
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
filePath = pathname.slice(1);
|
||||||
|
} else {
|
||||||
|
filePath = pathname;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// 26702执行forword file文件操作 不应该在这里乱来
|
||||||
|
// const cache = await dbUtil.getFileCacheByName(uri);
|
||||||
|
// if (cache) {
|
||||||
|
// filePath = cache.path;
|
||||||
|
// } else {
|
||||||
|
// filePath = uri;
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
res.isLocal = true;
|
||||||
|
}
|
||||||
|
// else{
|
||||||
|
// res.errMsg = `不支持的file协议,` + url.protocol
|
||||||
|
// return res
|
||||||
|
// }
|
||||||
|
// if (isGIF(filePath) && !res.isLocal) {
|
||||||
|
// await fs.rename(filePath, filePath + ".gif");
|
||||||
|
// filePath += ".gif";
|
||||||
|
// }
|
||||||
|
if (!res.isLocal && !res.ext) {
|
||||||
|
try {
|
||||||
|
const ext: string | undefined = (await fileType.fileTypeFromFile(filePath))?.ext;
|
||||||
|
if (ext) {
|
||||||
|
fs.renameSync(filePath, filePath + `.${ext}`);
|
||||||
|
filePath += `.${ext}`;
|
||||||
|
res.fileName += `.${ext}`;
|
||||||
|
res.ext = ext;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// log("获取文件类型失败", filePath,e.stack)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res.success = true;
|
||||||
|
res.path = filePath;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function copyFolder(sourcePath: string, destPath: string, logger: LogWrapper) {
|
||||||
|
try {
|
||||||
|
const entries = await fsPromise.readdir(sourcePath, { withFileTypes: true });
|
||||||
|
await fsPromise.mkdir(destPath, { recursive: true });
|
||||||
|
for (const entry of entries) {
|
||||||
|
const srcPath = path.join(sourcePath, entry.name);
|
||||||
|
const dstPath = path.join(destPath, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
await copyFolder(srcPath, dstPath, logger);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
await fsPromise.copyFile(srcPath, dstPath);
|
||||||
|
} catch (error) {
|
||||||
|
logger.logError(`无法复制文件 '${srcPath}' 到 '${dstPath}': ${error}`);
|
||||||
|
// 这里可以决定是否要继续复制其他文件
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.logError('复制文件夹时出错:', error);
|
||||||
|
}
|
||||||
|
}
|
@@ -1,9 +1,9 @@
|
|||||||
import {
|
import {
|
||||||
CacheFileListItem,
|
CacheFileListItem,
|
||||||
CacheFileType,
|
CacheFileType,
|
||||||
ChatCacheListItemBasic,
|
ChatCacheListItemBasic,
|
||||||
ChatType,
|
ChatType,
|
||||||
ElementType, IMAGE_HTTP_HOST, IMAGE_HTTP_HOST_NT, Peer, PicElement
|
ElementType, IMAGE_HTTP_HOST, IMAGE_HTTP_HOST_NT, Peer, PicElement
|
||||||
} from '@/core/entities';
|
} from '@/core/entities';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
@@ -14,83 +14,84 @@ import imageSize from 'image-size';
|
|||||||
import { ISizeCalculationResult } from 'image-size/dist/types/interface';
|
import { ISizeCalculationResult } from 'image-size/dist/types/interface';
|
||||||
import { NodeIKernelSearchService } from '../services/NodeIKernelSearchService';
|
import { NodeIKernelSearchService } from '../services/NodeIKernelSearchService';
|
||||||
import { RkeyManager } from '../helper/rkey';
|
import { RkeyManager } from '../helper/rkey';
|
||||||
|
import { calculateFileMD5 } from '@/common/utils/file';
|
||||||
|
|
||||||
|
|
||||||
export class NTQQFileApi {
|
export class NTQQFileApi {
|
||||||
context: InstanceContext;
|
context: InstanceContext;
|
||||||
core: NapCatCore;
|
core: NapCatCore;
|
||||||
rkeyManager: RkeyManager;
|
rkeyManager: RkeyManager;
|
||||||
constructor(context: InstanceContext, core: NapCatCore) {
|
constructor(context: InstanceContext, core: NapCatCore) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.core = core;
|
this.core = core;
|
||||||
this.rkeyManager = new RkeyManager('http://napcat-sign.wumiao.wang:2082/rkey', this.context.logger);
|
this.rkeyManager = new RkeyManager('http://napcat-sign.wumiao.wang:2082/rkey', this.context.logger);
|
||||||
}
|
}
|
||||||
async getFileType(filePath: string) {
|
async getFileType(filePath: string) {
|
||||||
return fileType.fileTypeFromFile(filePath);
|
return fileType.fileTypeFromFile(filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
async copyFile(filePath: string, destPath: string) {
|
async copyFile(filePath: string, destPath: string) {
|
||||||
await this.context.wrapper.util.copyFile(filePath, destPath);
|
await this.context.wrapper.util.copyFile(filePath, destPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFileSize(filePath: string): Promise<number> {
|
async getFileSize(filePath: string): Promise<number> {
|
||||||
return await this.context.wrapper.util.getFileSize(filePath);
|
return await this.context.wrapper.util.getFileSize(filePath);
|
||||||
}
|
}
|
||||||
async getVideoUrl(peer: Peer, msgId: string, elementId: string) {
|
async getVideoUrl(peer: Peer, msgId: string, elementId: string) {
|
||||||
return (await this.context.session.getRichMediaService().getVideoPlayUrlV2(peer, msgId, elementId, 0, { downSourceType: 1, triggerType: 1 })).urlResult.domainUrl;
|
return (await this.context.session.getRichMediaService().getVideoPlayUrlV2(peer, msgId, elementId, 0, { downSourceType: 1, triggerType: 1 })).urlResult.domainUrl;
|
||||||
}
|
}
|
||||||
// 上传文件到QQ的文件夹
|
// 上传文件到QQ的文件夹
|
||||||
async uploadFile(filePath: string, elementType: ElementType = ElementType.PIC, elementSubType: number = 0) {
|
async uploadFile(filePath: string, elementType: ElementType = ElementType.PIC, elementSubType: number = 0) {
|
||||||
// napCatCore.wrapper.util.
|
// napCatCore.wrapper.util.
|
||||||
const fileMd5 = await calculateFileMD5(filePath);
|
const fileMd5 = await calculateFileMD5(filePath);
|
||||||
let ext: string = (await this.getFileType(filePath))?.ext as string || '';
|
let ext: string = (await this.getFileType(filePath))?.ext as string || '';
|
||||||
if (ext) {
|
if (ext) {
|
||||||
ext = '.' + ext;
|
ext = '.' + ext;
|
||||||
}
|
|
||||||
let fileName = `${path.basename(filePath)}`;
|
|
||||||
if (fileName.indexOf('.') === -1) {
|
|
||||||
fileName += ext;
|
|
||||||
}
|
|
||||||
const mediaPath = this.context.session.getMsgService().getRichMediaFilePathForGuild({
|
|
||||||
md5HexStr: fileMd5,
|
|
||||||
fileName: fileName,
|
|
||||||
elementType: elementType,
|
|
||||||
elementSubType,
|
|
||||||
thumbSize: 0,
|
|
||||||
needCreate: true,
|
|
||||||
downloadType: 1,
|
|
||||||
file_uuid: ''
|
|
||||||
});
|
|
||||||
await this.copyFile(filePath, mediaPath!);
|
|
||||||
const fileSize = await this.getFileSize(filePath);
|
|
||||||
return {
|
|
||||||
md5: fileMd5,
|
|
||||||
fileName,
|
|
||||||
path: mediaPath,
|
|
||||||
fileSize,
|
|
||||||
ext
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
async downloadMediaByUuid() {
|
let fileName = `${path.basename(filePath)}`;
|
||||||
|
if (fileName.indexOf('.') === -1) {
|
||||||
|
fileName += ext;
|
||||||
|
}
|
||||||
|
const mediaPath = this.context.session.getMsgService().getRichMediaFilePathForGuild({
|
||||||
|
md5HexStr: fileMd5,
|
||||||
|
fileName: fileName,
|
||||||
|
elementType: elementType,
|
||||||
|
elementSubType,
|
||||||
|
thumbSize: 0,
|
||||||
|
needCreate: true,
|
||||||
|
downloadType: 1,
|
||||||
|
file_uuid: ''
|
||||||
|
});
|
||||||
|
await this.copyFile(filePath, mediaPath!);
|
||||||
|
const fileSize = await this.getFileSize(filePath);
|
||||||
|
return {
|
||||||
|
md5: fileMd5,
|
||||||
|
fileName,
|
||||||
|
path: mediaPath,
|
||||||
|
fileSize,
|
||||||
|
ext
|
||||||
|
};
|
||||||
|
}
|
||||||
|
async downloadMediaByUuid() {
|
||||||
//napCatCore.session.getRichMediaService().downloadFileForFileUuid();
|
//napCatCore.session.getRichMediaService().downloadFileForFileUuid();
|
||||||
}
|
}
|
||||||
async downloadMedia(msgId: string, chatType: ChatType, peerUid: string, elementId: string, thumbPath: string, sourcePath: string, timeout = 1000 * 60 * 2, force: boolean = false) {
|
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);
|
//logDebug('receive downloadMedia task', msgId, chatType, peerUid, elementId, thumbPath, sourcePath, timeout, force);
|
||||||
// 用于下载收到的消息中的图片等
|
// 用于下载收到的消息中的图片等
|
||||||
if (sourcePath && fs.existsSync(sourcePath)) {
|
if (sourcePath && fs.existsSync(sourcePath)) {
|
||||||
if (force) {
|
if (force) {
|
||||||
try {
|
try {
|
||||||
await fsPromises.unlink(sourcePath);
|
await fsPromises.unlink(sourcePath);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
//
|
//
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return sourcePath;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const data = await this.core.eventWrapper.CallNormalEvent<
|
} else {
|
||||||
|
return sourcePath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const data = await this.core.eventWrapper.CallNormalEvent<
|
||||||
(
|
(
|
||||||
params: {
|
params: {
|
||||||
fileModelId: string,
|
fileModelId: string,
|
||||||
downloadSourceType: number,
|
downloadSourceType: number,
|
||||||
triggerType: number,
|
triggerType: number,
|
||||||
@@ -103,111 +104,111 @@ export class NTQQFileApi {
|
|||||||
filePath: string
|
filePath: string
|
||||||
}) => Promise<unknown>,
|
}) => Promise<unknown>,
|
||||||
(fileTransNotifyInfo: OnRichMediaDownloadCompleteParams) => void
|
(fileTransNotifyInfo: OnRichMediaDownloadCompleteParams) => void
|
||||||
>(
|
>(
|
||||||
'NodeIKernelMsgService/downloadRichMedia',
|
'NodeIKernelMsgService/downloadRichMedia',
|
||||||
'NodeIKernelMsgListener/onRichMediaDownloadComplete',
|
'NodeIKernelMsgListener/onRichMediaDownloadComplete',
|
||||||
1,
|
1,
|
||||||
timeout,
|
timeout,
|
||||||
(arg: OnRichMediaDownloadCompleteParams) => {
|
(arg: OnRichMediaDownloadCompleteParams) => {
|
||||||
if (arg.msgId === msgId) {
|
if (arg.msgId === msgId) {
|
||||||
return true;
|
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;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
async getImageSize(filePath: string): Promise<ISizeCalculationResult | undefined> {
|
async getImageSize(filePath: string): Promise<ISizeCalculationResult | undefined> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
imageSize(filePath, (err, dimensions) => {
|
imageSize(filePath, (err, dimensions) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
reject(err);
|
reject(err);
|
||||||
} else {
|
|
||||||
resolve(dimensions);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
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 {
|
} else {
|
||||||
return undefined;
|
resolve(dimensions);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
return this.context.session.getSearchService().addSearchHistory({
|
});
|
||||||
type: 4,
|
}
|
||||||
contactList: [],
|
async addFileCache(peer: Peer, msgId: string, msgSeq: string, senderUid: string, elemId: string, elemType: string, fileSize: string, fileName: string) {
|
||||||
id: -1,
|
let GroupData;
|
||||||
groupInfos: [],
|
let BuddyData;
|
||||||
msgs: [],
|
if (peer.chatType === ChatType.group) {
|
||||||
fileInfos: [
|
GroupData =
|
||||||
{
|
[{
|
||||||
chatType: peer.chatType,
|
groupCode: peer.peerUid,
|
||||||
buddyChatInfo: BuddyData || [],
|
isConf: false,
|
||||||
discussChatInfo: [],
|
hasModifyConfGroupFace: true,
|
||||||
groupChatInfo: GroupData || [],
|
hasModifyConfGroupName: true,
|
||||||
dataLineChatInfo: [],
|
groupName: "NapCat.Cached",
|
||||||
tmpChatInfo: [],
|
remark: "NapCat.Cached"
|
||||||
msgId: msgId,
|
}];
|
||||||
msgSeq: msgSeq,
|
} else if (peer.chatType === ChatType.friend) {
|
||||||
msgTime: Math.floor(Date.now() / 1000).toString(),
|
BuddyData = [{
|
||||||
senderUid: senderUid,
|
category_name: 'NapCat.Cached',
|
||||||
senderNick: 'NapCat.Cached',
|
peerUid: peer.peerUid,
|
||||||
senderRemark: 'NapCat.Cached',
|
peerUin: peer.peerUid,
|
||||||
senderCard: 'NapCat.Cached',
|
remark: 'NapCat.Cached'
|
||||||
elemId: elemId,
|
}];
|
||||||
elemType: elemType,
|
} else {
|
||||||
fileSize: fileSize,
|
return undefined;
|
||||||
filePath: '',
|
|
||||||
fileName: fileName,
|
|
||||||
hits: [{
|
|
||||||
start: 12,
|
|
||||||
end: 14
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
async searchfile(keys: string[]) {
|
|
||||||
|
return this.context.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
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async searchfile(keys: string[]) {
|
||||||
type EventType = NodeIKernelSearchService['searchFileWithKeywords'];
|
type EventType = NodeIKernelSearchService['searchFileWithKeywords'];
|
||||||
interface OnListener {
|
interface OnListener {
|
||||||
searchId: string,
|
searchId: string,
|
||||||
@@ -250,98 +251,98 @@ export class NTQQFileApi {
|
|||||||
const Event = this.core.eventWrapper.createEventFunction<EventType>('NodeIKernelSearchService/searchFileWithKeywords');
|
const Event = this.core.eventWrapper.createEventFunction<EventType>('NodeIKernelSearchService/searchFileWithKeywords');
|
||||||
let id = '';
|
let id = '';
|
||||||
const Listener = this.core.eventWrapper.RegisterListen<(params: OnListener) => void>('NodeIKernelSearchListener/onSearchFileKeywordsResult', 1, 20000, (params) => {
|
const Listener = this.core.eventWrapper.RegisterListen<(params: OnListener) => void>('NodeIKernelSearchListener/onSearchFileKeywordsResult', 1, 20000, (params) => {
|
||||||
if (id !== '' && params.searchId == id) {
|
if (id !== '' && params.searchId == id) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
id = await Event!(keys, 12);
|
id = await Event!(keys, 12);
|
||||||
const [ret] = (await Listener);
|
const [ret] = (await Listener);
|
||||||
return ret;
|
return ret;
|
||||||
|
}
|
||||||
|
async getImageUrl(element: PicElement) {
|
||||||
|
if (!element) {
|
||||||
|
return '';
|
||||||
}
|
}
|
||||||
async getImageUrl(element: PicElement) {
|
const url: string = element.originImageUrl!; // 没有域名
|
||||||
if (!element) {
|
const md5HexStr = element.md5HexStr;
|
||||||
return '';
|
const fileMd5 = element.md5HexStr;
|
||||||
}
|
const fileUuid = element.fileUuid;
|
||||||
const url: string = element.originImageUrl!; // 没有域名
|
|
||||||
const md5HexStr = element.md5HexStr;
|
|
||||||
const fileMd5 = element.md5HexStr;
|
|
||||||
const fileUuid = element.fileUuid;
|
|
||||||
|
|
||||||
if (url) {
|
if (url) {
|
||||||
const UrlParse = new URL(IMAGE_HTTP_HOST + url);//临时解析拼接
|
const UrlParse = new URL(IMAGE_HTTP_HOST + url);//临时解析拼接
|
||||||
const imageAppid = UrlParse.searchParams.get('appid');
|
const imageAppid = UrlParse.searchParams.get('appid');
|
||||||
const isNewPic = imageAppid && ['1406', '1407'].includes(imageAppid);
|
const isNewPic = imageAppid && ['1406', '1407'].includes(imageAppid);
|
||||||
if (isNewPic) {
|
if (isNewPic) {
|
||||||
let UrlRkey = UrlParse.searchParams.get('rkey');
|
let UrlRkey = UrlParse.searchParams.get('rkey');
|
||||||
if (UrlRkey) {
|
if (UrlRkey) {
|
||||||
return IMAGE_HTTP_HOST_NT + url;
|
return IMAGE_HTTP_HOST_NT + url;
|
||||||
}
|
|
||||||
const rkeyData = await this.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`;
|
|
||||||
}
|
}
|
||||||
this.context.logger.logDebug('图片url获取失败', element);
|
const rkeyData = await this.rkeyManager.getRkey();
|
||||||
return '';
|
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`;
|
||||||
}
|
}
|
||||||
|
this.context.logger.logDebug('图片url获取失败', element);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NTQQFileCacheApi {
|
export class NTQQFileCacheApi {
|
||||||
context: InstanceContext;
|
context: InstanceContext;
|
||||||
core: NapCatCore;
|
core: NapCatCore;
|
||||||
constructor(context: InstanceContext, core: NapCatCore) {
|
constructor(context: InstanceContext, core: NapCatCore) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.core = core;
|
this.core = core;
|
||||||
}
|
}
|
||||||
async setCacheSilentScan(isSilent: boolean = true) {
|
async setCacheSilentScan(isSilent: boolean = true) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
getCacheSessionPathList() {
|
getCacheSessionPathList() {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
clearCache(cacheKeys: Array<string> = ['tmp', 'hotUpdate']) {
|
clearCache(cacheKeys: Array<string> = ['tmp', 'hotUpdate']) {
|
||||||
// 参数未验证
|
// 参数未验证
|
||||||
return this.context.session.getStorageCleanService().clearCacheDataByKeys(cacheKeys);
|
return this.context.session.getStorageCleanService().clearCacheDataByKeys(cacheKeys);
|
||||||
}
|
}
|
||||||
|
|
||||||
addCacheScannedPaths(pathMap: object = {}) {
|
addCacheScannedPaths(pathMap: object = {}) {
|
||||||
return this.context.session.getStorageCleanService().addCacheScanedPaths(pathMap);
|
return this.context.session.getStorageCleanService().addCacheScanedPaths(pathMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
scanCache() {
|
scanCache() {
|
||||||
//return (await this.context.session.getStorageCleanService().scanCache()).size;
|
//return (await this.context.session.getStorageCleanService().scanCache()).size;
|
||||||
}
|
}
|
||||||
|
|
||||||
getHotUpdateCachePath() {
|
getHotUpdateCachePath() {
|
||||||
// 未实现
|
// 未实现
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
getDesktopTmpPath() {
|
getDesktopTmpPath() {
|
||||||
// 未实现
|
// 未实现
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
getChatCacheList(type: ChatType, pageSize: number = 1000, pageIndex: number = 0) {
|
getChatCacheList(type: ChatType, pageSize: number = 1000, pageIndex: number = 0) {
|
||||||
return this.context.session.getStorageCleanService().getChatCacheInfo(type, pageSize, 1, pageIndex);
|
return this.context.session.getStorageCleanService().getChatCacheInfo(type, pageSize, 1, pageIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
getFileCacheInfo(fileType: CacheFileType, pageSize: number = 1000, lastRecord?: CacheFileListItem) {
|
getFileCacheInfo(fileType: CacheFileType, pageSize: number = 1000, lastRecord?: CacheFileListItem) {
|
||||||
const _lastRecord = lastRecord ? lastRecord : { fileType: fileType };
|
const _lastRecord = lastRecord ? lastRecord : { fileType: fileType };
|
||||||
//需要五个参数
|
//需要五个参数
|
||||||
//return napCatCore.session.getStorageCleanService().getFileCacheInfo();
|
//return napCatCore.session.getStorageCleanService().getFileCacheInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
async clearChatCache(chats: ChatCacheListItemBasic[] = [], fileKeys: string[] = []) {
|
async clearChatCache(chats: ChatCacheListItemBasic[] = [], fileKeys: string[] = []) {
|
||||||
return this.context.session.getStorageCleanService().clearChatCacheInfo(chats, fileKeys);
|
return this.context.session.getStorageCleanService().clearChatCacheInfo(chats, fileKeys);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -9,7 +9,7 @@ import { sleep } from "@/common/utils/helper";
|
|||||||
import { SelfInfo, LineDevice, SelfStatusInfo } from "./entities";
|
import { SelfInfo, LineDevice, SelfStatusInfo } from "./entities";
|
||||||
import { LegacyNTEventWrapper } from "@/common/framework/event-legacy";
|
import { LegacyNTEventWrapper } from "@/common/framework/event-legacy";
|
||||||
import { NTQQFriendApi, NTQQGroupApi, NTQQMsgApi, NTQQUserApi, NTQQWebApi } from "./apis";
|
import { NTQQFriendApi, NTQQGroupApi, NTQQMsgApi, NTQQUserApi, NTQQWebApi } from "./apis";
|
||||||
|
import os from "node:os";
|
||||||
export enum NapCatCoreWorkingEnv {
|
export enum NapCatCoreWorkingEnv {
|
||||||
Unknown = 0,
|
Unknown = 0,
|
||||||
Shell = 1,
|
Shell = 1,
|
||||||
@@ -31,7 +31,8 @@ export class NapCatCore {
|
|||||||
readonly ApiContext: NTApiContext;
|
readonly ApiContext: NTApiContext;
|
||||||
readonly eventWrapper: LegacyNTEventWrapper;
|
readonly eventWrapper: LegacyNTEventWrapper;
|
||||||
// readonly eventChannel: NTEventChannel;
|
// readonly eventChannel: NTEventChannel;
|
||||||
|
NapCatDataPath: string;
|
||||||
|
NapCatTempPath: string;
|
||||||
// runtime info, not readonly
|
// runtime info, not readonly
|
||||||
selfInfo: SelfInfo;
|
selfInfo: SelfInfo;
|
||||||
// 通过构造器递过去的 runtime info 应该尽量少
|
// 通过构造器递过去的 runtime info 应该尽量少
|
||||||
@@ -47,10 +48,26 @@ export class NapCatCore {
|
|||||||
UserApi: new NTQQUserApi(this.context, this),
|
UserApi: new NTQQUserApi(this.context, this),
|
||||||
GroupApi: new NTQQGroupApi(this.context, this)
|
GroupApi: new NTQQGroupApi(this.context, this)
|
||||||
};
|
};
|
||||||
|
this.NapCatDataPath = path.join(this.dataPath, 'NapCat');
|
||||||
|
fs.mkdirSync(this.NapCatDataPath, { recursive: true });
|
||||||
|
this.NapCatTempPath = path.join(this.NapCatDataPath, 'temp');
|
||||||
|
// 创建临时目录
|
||||||
|
if (!fs.existsSync(this.NapCatTempPath)) {
|
||||||
|
fs.mkdirSync(this.NapCatTempPath, { recursive: true });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
getApiContext() {
|
getApiContext() {
|
||||||
return this.ApiContext;
|
return this.ApiContext;
|
||||||
}
|
}
|
||||||
|
get dataPath(): string {
|
||||||
|
let result = this.context.wrapper.util.getNTUserDataInfoConfig();
|
||||||
|
if (!result) {
|
||||||
|
result = path.resolve(os.homedir(), './.config/QQ');
|
||||||
|
fs.mkdirSync(result, { recursive: true });
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
// Renamed from 'InitDataListener'
|
// Renamed from 'InitDataListener'
|
||||||
async initNapCatCoreListeners() {
|
async initNapCatCoreListeners() {
|
||||||
const msgListener = new MsgListener();
|
const msgListener = new MsgListener();
|
||||||
|
Reference in New Issue
Block a user