diff --git a/src/common/utils/audio.ts b/src/common/utils/audio.ts new file mode 100644 index 00000000..d9f7308d --- /dev/null +++ b/src/common/utils/audio.ts @@ -0,0 +1,88 @@ +import fs from 'fs'; +import { encode, getDuration, getWavFileInfo, isWav, isSilk } from 'silk-wasm'; +import fsPromise from 'fs/promises'; +import path from 'node:path'; +import { randomUUID } from 'crypto'; +import { spawn } from 'node:child_process'; +import { LogWrapper } from './log'; +export async function encodeSilk(filePath: string, TEMP_DIR: string, logger: LogWrapper) { + async function guessDuration(pttPath: string) { + const pttFileInfo = await fsPromise.stat(pttPath); + let duration = pttFileInfo.size / 1024 / 3; // 3kb/s + duration = Math.floor(duration); + duration = Math.max(1, duration); + logger.log('通过文件大小估算语音的时长:', duration); + return duration; + } + try { + const file = await fsPromise.readFile(filePath); + const pttPath = path.join(TEMP_DIR, randomUUID()); + if (!isSilk(file)) { + logger.log(`语音文件${filePath}需要转换成silk`); + const _isWav = isWav(file); + const pcmPath = pttPath + '.pcm'; + let sampleRate = 0; + const convert = () => { + return new Promise((resolve, reject) => { + // todo: 通过配置文件获取ffmpeg路径 + const ffmpegPath = process.env.FFMPEG_PATH || 'ffmpeg'; + const cp = spawn(ffmpegPath, ['-y', '-i', filePath, '-ar', '24000', '-ac', '1', '-f', 's16le', pcmPath]); + cp.on('error', err => { + logger.log('FFmpeg处理转换出错: ', err.message); + return reject(err); + }); + cp.on('exit', (code, signal) => { + const EXIT_CODES = [0, 255]; + if (code == null || EXIT_CODES.includes(code)) { + sampleRate = 24000; + const data = fs.readFileSync(pcmPath); + fs.unlink(pcmPath, (err) => { + }); + return resolve(data); + } + logger.log(`FFmpeg exit: code=${code ?? 'unknown'} sig=${signal ?? 'unknown'}`); + reject(Error('FFmpeg处理转换失败')); + }); + }); + }; + let input: Buffer; + if (!_isWav) { + input = await convert(); + } else { + input = file; + const allowSampleRate = [8000, 12000, 16000, 24000, 32000, 44100, 48000]; + const { fmt } = getWavFileInfo(input); + // log(`wav文件信息`, fmt) + if (!allowSampleRate.includes(fmt.sampleRate)) { + input = await convert(); + } + } + const silk = await encode(input, sampleRate); + fs.writeFileSync(pttPath, silk.data); + logger.log(`语音文件${filePath}转换成功!`, pttPath, '时长:', silk.duration); + return { + converted: true, + path: pttPath, + duration: silk.duration / 1000 + }; + } else { + const silk = file; + let duration = 0; + try { + duration = getDuration(silk) / 1000; + } catch (e: any) { + logger.log('获取语音文件时长失败, 使用文件大小推测时长', filePath, e.stack); + duration = await guessDuration(filePath); + } + + return { + converted: false, + path: filePath, + duration, + }; + } + } catch (error: any) { + logger.logError('convert silk failed', error.stack); + return {}; + } +} diff --git a/src/common/utils/video.ts b/src/common/utils/video.ts new file mode 100644 index 00000000..5a1fc9f0 --- /dev/null +++ b/src/common/utils/video.ts @@ -0,0 +1,62 @@ +import ffmpeg, { FfprobeStream } from 'fluent-ffmpeg'; +import fs from 'fs'; +import { LogWrapper } from './log'; + +const defaultVideoThumbB64 = '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'; + +export const defaultVideoThumb = Buffer.from(defaultVideoThumbB64, 'base64'); + +export async function getVideoInfo(filePath: string,logger:LogWrapper) { + const size = fs.statSync(filePath).size; + return new Promise<{ + width: number, + height: number, + time: number, + format: string, + size: number, + filePath: string + }>((resolve, reject) => { + const ffmpegPath = process.env.FFMPEG_PATH; + ffmpegPath && ffmpeg.setFfmpegPath(ffmpegPath); + ffmpeg(filePath).ffprobe((err: any, metadata: ffmpeg.FfprobeData) => { + if (err) { + reject(err); + } else { + const videoStream = metadata.streams.find((s: FfprobeStream) => s.codec_type === 'video'); + if (videoStream) { + logger.log(`视频尺寸: ${videoStream.width}x${videoStream.height}`); + } else { + return reject('未找到视频流信息。'); + } + resolve({ + width: videoStream.width!, height: videoStream.height!, + time: parseInt(videoStream.duration!), + format: metadata.format.format_name!, + size, + filePath + }); + } + }); + }); +} +export function checkFfmpeg(newPath: string | null = null,logger:LogWrapper): Promise { + return new Promise((resolve, reject) => { + logger.log('开始检查ffmpeg', newPath); + if (newPath) { + ffmpeg.setFfmpegPath(newPath); + } + try { + ffmpeg.getAvailableFormats((err: any, formats: any) => { + if (err) { + logger.log('ffmpeg is not installed or not found in PATH:', err); + resolve(false); + } else { + logger.log('ffmpeg is installed.'); + resolve(true); + } + }); + } catch (e) { + resolve(false); + } + }); +} diff --git a/src/onebot/action/go-cqhttp/DownloadFile.ts b/src/onebot/action/go-cqhttp/DownloadFile.ts index c3dfb15b..11cb7101 100644 --- a/src/onebot/action/go-cqhttp/DownloadFile.ts +++ b/src/onebot/action/go-cqhttp/DownloadFile.ts @@ -32,7 +32,7 @@ export default class GoCQHTTPDownloadFile extends BaseAction { const isRandomName = !payload.name; const name = payload.name || randomUUID(); - const filePath = joinPath(getTempDir(), name); + const filePath = joinPath(this.CoreContext.NapCatTempPath, name); if (payload.base64) { fs.writeFileSync(filePath, payload.base64, 'base64'); @@ -48,7 +48,7 @@ export default class GoCQHTTPDownloadFile extends BaseAction { const msgList = data.msgList; const messages = await Promise.all(msgList.map(async msg => { const resMsg = await OB11Constructor.message(msg); - resMsg.message_id = await MessageUnique.createMsg({ guildId:'',chatType:msg.chatType,peerUid:msg.peerUid },msg.msgId)!; + resMsg.message_id = MessageUnique.createMsg({ guildId:'',chatType:msg.chatType,peerUid:msg.peerUid },msg.msgId)!; return resMsg; })); messages.map(msg => { diff --git a/src/onebot/action/go-cqhttp/GetFriendMsgHistory.ts b/src/onebot/action/go-cqhttp/GetFriendMsgHistory.ts index ef98bf46..b899cb2b 100644 --- a/src/onebot/action/go-cqhttp/GetFriendMsgHistory.ts +++ b/src/onebot/action/go-cqhttp/GetFriendMsgHistory.ts @@ -3,7 +3,7 @@ import { OB11Message, OB11User } from '../../types'; import { ActionName } from '../types'; import { ChatType, RawMessage } from '@/core/entities'; import { NTQQMsgApi } from '@/core/apis/msg'; -import { OB11Constructor } from '../../helper/constructor'; +import { OB11Constructor } from '../../helper/data'; import { FromSchema, JSONSchema } from 'json-schema-to-ts'; import { MessageUnique } from '@/common/utils/MessageUnique'; diff --git a/src/onebot/action/go-cqhttp/GetGroupHonorInfo.ts b/src/onebot/action/go-cqhttp/GetGroupHonorInfo.ts index bd60031f..9669a709 100644 --- a/src/onebot/action/go-cqhttp/GetGroupHonorInfo.ts +++ b/src/onebot/action/go-cqhttp/GetGroupHonorInfo.ts @@ -7,7 +7,7 @@ const SchemaData = { type: 'object', properties: { group_id: { type: [ 'number' , 'string' ] }, - type: { enum: [WebHonorType.ALL, WebHonorType.EMOTION, WebHonorType.LEGEND, WebHonorType.PERFROMER, WebHonorType.STORONGE_NEWBI, WebHonorType.TALKACTIVE] } + type: { enum: [WebHonorType.ALL, WebHonorType.EMOTION, WebHonorType.LEGEND, WebHonorType.PERFORMER, WebHonorType.STRONG_NEWBIE, WebHonorType.TALKATIVE] } }, required: ['group_id'] } as const satisfies JSONSchema; diff --git a/src/onebot/action/go-cqhttp/GetGroupMsgHistory.ts b/src/onebot/action/go-cqhttp/GetGroupMsgHistory.ts index bb7e9b7a..fb6af824 100644 --- a/src/onebot/action/go-cqhttp/GetGroupMsgHistory.ts +++ b/src/onebot/action/go-cqhttp/GetGroupMsgHistory.ts @@ -1,9 +1,8 @@ import BaseAction from '../BaseAction'; -import { OB11Message, OB11User } from '../../types'; +import { OB11Message } from '../../types'; import { ActionName } from '../types'; import { ChatType, Peer, RawMessage } from '@/core/entities'; -import { NTQQMsgApi } from '@/core/apis/msg'; -import { OB11Constructor } from '../../helper/constructor'; +import { OB11Constructor } from '../../helper/data'; import { FromSchema, JSONSchema } from 'json-schema-to-ts'; import { MessageUnique } from '@/common/utils/MessageUnique'; interface Response { @@ -41,7 +40,7 @@ export default class GoCQHTTPGetGroupMsgHistory extends BaseAction { msg.id = MessageUnique.createMsg({ guildId: '', chatType: msg.chatType, peerUid: msg.peerUid }, msg.msgId); })); diff --git a/src/onebot/action/go-cqhttp/GetOnlineClient.ts b/src/onebot/action/go-cqhttp/GetOnlineClient.ts index 22937d15..85643e5b 100644 --- a/src/onebot/action/go-cqhttp/GetOnlineClient.ts +++ b/src/onebot/action/go-cqhttp/GetOnlineClient.ts @@ -1,8 +1,6 @@ -import { DeviceList } from '@/onebot11/main'; import BaseAction from '../BaseAction'; import { ActionName } from '../types'; import { JSONSchema } from 'json-schema-to-ts'; -import { NTQQSystemApi } from '@/core'; import { sleep } from '@/common/utils/helper'; const SchemaData = { @@ -16,8 +14,11 @@ export class GetOnlineClient extends BaseAction> { actionName = ActionName.GetOnlineClient; protected async _handle(payload: void) { + //注册监听 + const NTQQSystemApi = this.CoreContext.getApiContext().SystemApi; NTQQSystemApi.getOnlineDev(); await sleep(500); - return DeviceList; + + return []; } } diff --git a/src/onebot/action/go-cqhttp/GetStrangerInfo.ts b/src/onebot/action/go-cqhttp/GetStrangerInfo.ts index 3848fc97..e56cd79f 100644 --- a/src/onebot/action/go-cqhttp/GetStrangerInfo.ts +++ b/src/onebot/action/go-cqhttp/GetStrangerInfo.ts @@ -1,8 +1,7 @@ import BaseAction from '../BaseAction'; import { OB11User, OB11UserSex } from '../../types'; -import { OB11Constructor } from '../../helper/constructor'; +import { OB11Constructor } from '../../helper/data'; import { ActionName } from '../types'; -import { NTQQUserApi } from '@/core/apis/user'; import { FromSchema, JSONSchema } from 'json-schema-to-ts'; import { calcQQLevel } from '@/common/utils/qqlevel'; const SchemaData = { @@ -20,24 +19,24 @@ export default class GoCQHTTPGetStrangerInfo extends BaseAction { const NTQQUserApi = this.CoreContext.getApiContext().UserApi; - const user_id = payload.user_id.toString(); - const extendData = await NTQQUserApi.getUserDetailInfoByUin(user_id); - const uid = (await NTQQUserApi.getUidByUin(user_id))!; - if (!uid || uid.indexOf('*') != -1) { - const ret = { - ...extendData, - user_id: parseInt(extendData.info.uin) || 0, - nickname: extendData.info.nick, - sex: OB11UserSex.unknown, - age: (extendData.info.birthday_year == 0) ? 0 : new Date().getFullYear() - extendData.info.birthday_year, - qid: extendData.info.qid, - level: extendData.info.qqLevel && calcQQLevel(extendData.info.qqLevel) || 0, - login_days: 0, - uid: '' - }; - return ret; - } - const data = { ...extendData, ...(await NTQQUserApi.getUserDetailInfo(uid)) }; - return OB11Constructor.stranger(data); + const user_id = payload.user_id.toString(); + const extendData = await NTQQUserApi.getUserDetailInfoByUin(user_id); + const uid = (await NTQQUserApi.getUidByUin(user_id))!; + if (!uid || uid.indexOf('*') != -1) { + const ret = { + ...extendData, + user_id: parseInt(extendData.info.uin) || 0, + nickname: extendData.info.nick, + sex: OB11UserSex.unknown, + age: (extendData.info.birthday_year == 0) ? 0 : new Date().getFullYear() - extendData.info.birthday_year, + qid: extendData.info.qid, + level: extendData.info.qqLevel && calcQQLevel(extendData.info.qqLevel) || 0, + login_days: 0, + uid: '' + }; + return ret; + } + const data = { ...extendData, ...(await NTQQUserApi.getUserDetailInfo(uid)) }; + return OB11Constructor.stranger(data); } } diff --git a/src/onebot/action/go-cqhttp/QuickAction.ts b/src/onebot/action/go-cqhttp/QuickAction.ts index 5dc5df00..43fc8983 100644 --- a/src/onebot/action/go-cqhttp/QuickAction.ts +++ b/src/onebot/action/go-cqhttp/QuickAction.ts @@ -10,7 +10,7 @@ interface Payload{ export class GoCQHTTPHandleQuickAction extends BaseAction{ actionName = ActionName.GoCQHTTP_HandleQuickAction; protected async _handle(payload: Payload): Promise { - handleQuickOperation(payload.context, payload.operation).then().catch(log); + handleQuickOperation(payload.context, payload.operation,this.CoreContext).then().catch(this.CoreContext.context.logger.logError); return null; } } \ No newline at end of file diff --git a/src/onebot/action/go-cqhttp/SendGroupNotice.ts b/src/onebot/action/go-cqhttp/SendGroupNotice.ts index 2e1891fa..2924550f 100644 --- a/src/onebot/action/go-cqhttp/SendGroupNotice.ts +++ b/src/onebot/action/go-cqhttp/SendGroupNotice.ts @@ -1,7 +1,6 @@ import { checkFileReceived, uri2local } from '@/common/utils/file'; import BaseAction from '../BaseAction'; import { ActionName } from '../types'; -import { NTQQGroupApi, WebApi } from '@/core/apis'; import { unlink } from 'node:fs'; import { FromSchema, JSONSchema } from 'json-schema-to-ts'; const SchemaData = { @@ -25,7 +24,7 @@ export class SendGroupNotice extends BaseAction { let UploadImage: { id: string, width: number, height: number } | undefined = undefined; if (payload.image) { //公告图逻辑 - const { errMsg, path, isLocal, success } = (await uri2local(payload.image)); + const { errMsg, path, isLocal, success } = (await uri2local(this.CoreContext.NapCatTempPath,payload.image)); if (!success) { throw `群公告${payload.image}设置失败,image字段可能格式不正确`; } diff --git a/src/onebot/action/go-cqhttp/UploadGroupFile.ts b/src/onebot/action/go-cqhttp/UploadGroupFile.ts index 90f33c1c..be1eb2e9 100644 --- a/src/onebot/action/go-cqhttp/UploadGroupFile.ts +++ b/src/onebot/action/go-cqhttp/UploadGroupFile.ts @@ -5,6 +5,7 @@ import fs from 'fs'; import { sendMsg } from '@/onebot/action/msg/SendMsg'; import { uri2local } from '@/common/utils/file'; import { FromSchema, JSONSchema } from 'json-schema-to-ts'; +import { SendMsgElementConstructor } from '@/onebot/helper/msg'; const SchemaData = { type: 'object', properties: { @@ -27,12 +28,12 @@ export default class GoCQHTTPUploadGroupFile extends BaseAction { if (fs.existsSync(file)) { file = `file://${file}`; } - const downloadResult = await uri2local(file); + const downloadResult = await uri2local(this.CoreContext.NapCatTempPath, file); if (!downloadResult.success) { throw new Error(downloadResult.errMsg); } - const sendFileEle: SendFileElement = await SendMsgElementConstructor.file(downloadResult.path, payload.name, payload.folder_id); - await sendMsg({ chatType: ChatType.group, peerUid: group.groupCode }, [sendFileEle], [], true); + const sendFileEle: SendFileElement = await SendMsgElementConstructor.file(this.CoreContext, downloadResult.path, payload.name, payload.folder_id); + await sendMsg({ chatType: ChatType.group, peerUid: payload.group_id.toString() }, [sendFileEle], [], true); return null; } } diff --git a/src/onebot/action/go-cqhttp/UploadPrivareFile.ts b/src/onebot/action/go-cqhttp/UploadPrivareFile.ts index 368c917c..2a88e033 100644 --- a/src/onebot/action/go-cqhttp/UploadPrivareFile.ts +++ b/src/onebot/action/go-cqhttp/UploadPrivareFile.ts @@ -2,10 +2,10 @@ import BaseAction from '../BaseAction';; import { ActionName } from '../types'; import { ChatType, Peer, SendFileElement } from '@/core/entities'; import fs from 'fs'; -import { SendMsg, sendMsg } from '@/onebot/action/msg/SendMsg'; +import { sendMsg } from '@/onebot/action/msg/SendMsg'; import { uri2local } from '@/common/utils/file'; import { FromSchema, JSONSchema } from 'json-schema-to-ts'; -import { NTQQFriendApi, NTQQUserApi } from '@/core'; +import { SendMsgElementConstructor } from '@/onebot/helper/msg'; const SchemaData = { type: 'object', properties: { @@ -22,6 +22,8 @@ export default class GoCQHTTPUploadPrivateFile extends BaseAction actionName = ActionName.GOCQHTTP_UploadPrivateFile; PayloadSchema = SchemaData; async getPeer(payload: Payload): Promise { + const NTQQUserApi = this.CoreContext.getApiContext().UserApi; + const NTQQFriendApi = this.CoreContext.getApiContext().FriendApi; if (payload.user_id) { const peerUid = await NTQQUserApi.getUidByUin(payload.user_id.toString()); if (!peerUid) { @@ -38,11 +40,11 @@ export default class GoCQHTTPUploadPrivateFile extends BaseAction if (fs.existsSync(file)) { file = `file://${file}`; } - const downloadResult = await uri2local(file); + const downloadResult = await uri2local(this.CoreContext.NapCatTempPath, file); if (!downloadResult.success) { throw new Error(downloadResult.errMsg); } - const sendFileEle: SendFileElement = await SendMsgElementConstructor.file(downloadResult.path, payload.name); + const sendFileEle: SendFileElement = await SendMsgElementConstructor.file(this.CoreContext, downloadResult.path, payload.name); await sendMsg(peer, [sendFileEle], [], true); return null; } diff --git a/src/onebot/action/group/GetGroupInfo.ts b/src/onebot/action/group/GetGroupInfo.ts index 21f96bc9..dc969b06 100644 --- a/src/onebot/action/group/GetGroupInfo.ts +++ b/src/onebot/action/group/GetGroupInfo.ts @@ -1,5 +1,5 @@ import { OB11Group } from '../../types'; -import { OB11Constructor } from '../../helper/constructor'; +import { OB11Constructor } from '../../helper/data'; import BaseAction from '../BaseAction'; import { ActionName } from '../types'; import { FromSchema, JSONSchema } from 'json-schema-to-ts'; diff --git a/src/onebot/action/group/GetGroupList.ts b/src/onebot/action/group/GetGroupList.ts index 6b8e2823..05721085 100644 --- a/src/onebot/action/group/GetGroupList.ts +++ b/src/onebot/action/group/GetGroupList.ts @@ -1,5 +1,5 @@ import { OB11Group } from '../../types'; -import { OB11Constructor } from '../../helper/constructor'; +import { OB11Constructor } from '../../helper/data'; import BaseAction from '../BaseAction'; import { ActionName } from '../types'; import { Group } from '@/core/entities'; diff --git a/src/onebot/action/group/GetGroupMemberInfo.ts b/src/onebot/action/group/GetGroupMemberInfo.ts index cd2058dc..c51409bf 100644 --- a/src/onebot/action/group/GetGroupMemberInfo.ts +++ b/src/onebot/action/group/GetGroupMemberInfo.ts @@ -1,5 +1,5 @@ import { OB11GroupMember } from '../../types'; -import { OB11Constructor } from '../../helper/constructor'; +import { OB11Constructor } from '../../helper/data'; import BaseAction from '../BaseAction'; import { ActionName } from '../types'; import { FromSchema, JSONSchema } from 'json-schema-to-ts'; diff --git a/src/onebot/action/group/GetGroupMemberInfoOld.ts b/src/onebot/action/group/GetGroupMemberInfoOld.ts index cb42f9b9..ebe51e82 100644 --- a/src/onebot/action/group/GetGroupMemberInfoOld.ts +++ b/src/onebot/action/group/GetGroupMemberInfoOld.ts @@ -1,5 +1,5 @@ import { OB11GroupMember } from '../../types'; -import { OB11Constructor } from '../../helper/constructor'; +import { OB11Constructor } from '../../helper/data'; import BaseAction from '../BaseAction'; import { ActionName } from '../types'; import { NTQQUserApi } from '@/core/apis/user'; diff --git a/src/onebot/action/group/GetGroupMemberList.ts b/src/onebot/action/group/GetGroupMemberList.ts index 1e6076d2..1f067305 100644 --- a/src/onebot/action/group/GetGroupMemberList.ts +++ b/src/onebot/action/group/GetGroupMemberList.ts @@ -1,6 +1,6 @@ import { OB11GroupMember } from '../../types'; -import { OB11Constructor } from '../../helper/constructor'; +import { OB11Constructor } from '../../helper/data'; import BaseAction from '../BaseAction'; import { ActionName } from '../types'; import { FromSchema, JSONSchema } from 'json-schema-to-ts'; diff --git a/src/onebot/action/msg/GetMsg.ts b/src/onebot/action/msg/GetMsg.ts index 189114fe..62a80bdb 100644 --- a/src/onebot/action/msg/GetMsg.ts +++ b/src/onebot/action/msg/GetMsg.ts @@ -1,5 +1,5 @@ import { OB11Message } from '../../types'; -import { OB11Constructor } from '../../helper/constructor'; +import { OB11Constructor } from '../../helper/data'; import BaseAction from '../BaseAction'; import { ActionName } from '../types'; import { FromSchema, JSONSchema } from 'json-schema-to-ts'; diff --git a/src/onebot/action/system/GetLoginInfo.ts b/src/onebot/action/system/GetLoginInfo.ts index 4a73664b..6dcbfaca 100644 --- a/src/onebot/action/system/GetLoginInfo.ts +++ b/src/onebot/action/system/GetLoginInfo.ts @@ -1,6 +1,6 @@ import { OB11User } from '../../types'; -import { OB11Constructor } from '../../helper/constructor'; +import { OB11Constructor } from '../../helper/data'; import BaseAction from '../BaseAction'; import { ActionName } from '../types'; diff --git a/src/onebot/helper/constructor.ts b/src/onebot/helper/data.ts similarity index 100% rename from src/onebot/helper/constructor.ts rename to src/onebot/helper/data.ts diff --git a/src/onebot/helper/msg.ts b/src/onebot/helper/msg.ts new file mode 100644 index 00000000..f0e1a757 --- /dev/null +++ b/src/onebot/helper/msg.ts @@ -0,0 +1,377 @@ +import { + AtType, + ElementType, FaceIndex, FaceType, NapCatCore, PicElement, + PicType, + SendArkElement, + SendFaceElement, + SendFileElement, SendMarkdownElement, SendMarketFaceElement, + SendPicElement, + SendPttElement, + SendReplyElement, + sendShareLocationElement, + SendTextElement, + SendVideoElement, + viedo_type +} from '@/core'; +import { promises as fs } from 'node:fs'; +import ffmpeg from 'fluent-ffmpeg'; +import { NTQQFileApi } from '@/core/apis/file'; +import { calculateFileMD5, isGIF } from '@/common/utils/file'; +import { defaultVideoThumb, getVideoInfo } from '@/common/utils/video'; +import { encodeSilk } from '@/common/utils/audio'; +import { isNull } from '@/common/utils/helper'; +import faceConfig from '@/core/external/face_config.json'; +import * as pathLib from 'node:path'; +export class SendMsgElementConstructor { + static location(CoreContext: NapCatCore): sendShareLocationElement { + return { + elementType: ElementType.SHARELOCATION, + elementId: '', + shareLocationElement: { + text: "测试", + ext: "" + } + } + } + static text(CoreContext: NapCatCore, content: string): SendTextElement { + return { + elementType: ElementType.TEXT, + elementId: '', + textElement: { + content, + atType: AtType.notAt, + atUid: '', + atTinyId: '', + atNtUid: '', + }, + }; + } + + static at(CoreContext: NapCatCore, atUid: string, atNtUid: string, atType: AtType, atName: string): SendTextElement { + return { + elementType: ElementType.TEXT, + elementId: '', + textElement: { + content: `@${atName}`, + atType, + atUid, + atTinyId: '', + atNtUid, + }, + }; + } + + static reply(CoreContext: NapCatCore, msgSeq: string, msgId: string, senderUin: string, senderUinStr: string): SendReplyElement { + return { + elementType: ElementType.REPLY, + elementId: '', + replyElement: { + replayMsgSeq: msgSeq, // raw.msgSeq + replayMsgId: msgId, // raw.msgId + senderUin: senderUin, + senderUinStr: senderUinStr, + } + }; + } + + static async pic(CoreContext: NapCatCore, picPath: string, summary: string = '', subType: 0 | 1 = 0): Promise { + const { md5, fileName, path, fileSize } = await NTQQFileApi.uploadFile(picPath, ElementType.PIC, subType); + if (fileSize === 0) { + throw '文件异常,大小为0'; + } + const imageSize = await NTQQFileApi.getImageSize(picPath); + const picElement: any = { + md5HexStr: md5, + fileSize: fileSize.toString(), + picWidth: imageSize?.width, + picHeight: imageSize?.height, + fileName: fileName, + sourcePath: path, + original: true, + picType: isGIF(picPath) ? PicType.gif : PicType.jpg, + picSubType: subType, + fileUuid: '', + fileSubId: '', + thumbFileSize: 0, + summary + }; + //logDebug('图片信息', picElement); + return { + elementType: ElementType.PIC, + elementId: '', + picElement, + }; + } + + static async file(CoreContext: NapCatCore, filePath: string, fileName: string = '', folderId: string = ''): Promise { + const { md5, fileName: _fileName, path, fileSize } = await NTQQFileApi.uploadFile(filePath, ElementType.FILE); + if (fileSize === 0) { + throw '文件异常,大小为0'; + } + const element: SendFileElement = { + elementType: ElementType.FILE, + elementId: '', + fileElement: { + fileName: fileName || _fileName, + folderId: folderId, + 'filePath': path!, + 'fileSize': (fileSize).toString(), + } + }; + + return element; + } + + static async video(CoreContext: NapCatCore, filePath: string, fileName: string = '', diyThumbPath: string = '', videotype: viedo_type = viedo_type.VIDEO_FORMAT_MP4): Promise { + const { fileName: _fileName, path, fileSize, md5 } = await NTQQFileApi.uploadFile(filePath, ElementType.VIDEO); + if (fileSize === 0) { + throw '文件异常,大小为0'; + } + let thumb = path.replace(`${pathLib.sep}Ori${pathLib.sep}`, `${pathLib.sep}Thumb${pathLib.sep}`); + thumb = pathLib.dirname(thumb); + // log("thumb 目录", thumb) + let videoInfo = { + width: 1920, height: 1080, + time: 15, + format: 'mp4', + size: fileSize, + filePath + }; + try { + videoInfo = await getVideoInfo(path); + //logDebug('视频信息', videoInfo); + } catch (e) { + logError('获取视频信息失败', e); + } + const createThumb = new Promise((resolve, reject) => { + const thumbFileName = `${md5}_0.png`; + const thumbPath = pathLib.join(thumb, thumbFileName); + ffmpeg(filePath) + .on('end', () => { + }) + .on('error', (err) => { + logDebug('获取视频封面失败,使用默认封面', err); + if (diyThumbPath) { + fs.copyFile(diyThumbPath, thumbPath).then(() => { + resolve(thumbPath); + }).catch(reject); + } else { + fs.writeFile(thumbPath, defaultVideoThumb).then(() => { + resolve(thumbPath); + }).catch(reject); + } + }) + .screenshots({ + timestamps: [0], + filename: thumbFileName, + folder: thumb, + size: videoInfo.width + 'x' + videoInfo.height + }).on('end', () => { + resolve(thumbPath); + }); + }); + const thumbPath = new Map(); + const _thumbPath = await createThumb; + const thumbSize = (await fs.stat(_thumbPath)).size; + // log("生成缩略图", _thumbPath) + thumbPath.set(0, _thumbPath); + const thumbMd5 = await calculateFileMD5(_thumbPath); + const element: SendVideoElement = { + elementType: ElementType.VIDEO, + elementId: '', + videoElement: { + fileName: fileName || _fileName, + filePath: path, + videoMd5: md5, + thumbMd5, + fileTime: videoInfo.time, + thumbPath: thumbPath, + thumbSize, + thumbWidth: videoInfo.width, + thumbHeight: videoInfo.height, + fileSize: '' + fileSize, + //fileFormat: videotype + // fileUuid: "", + // transferStatus: 0, + // progress: 0, + // invalidState: 0, + // fileSubId: "", + // fileBizId: null, + // originVideoMd5: "", + // fileFormat: 2, + // import_rich_media_context: null, + // sourceVideoCodecFormat: 2 + } + }; + return element; + } + + static async ptt(CoreContext: NapCatCore, pttPath: string): Promise { + const { converted, path: silkPath, duration } = await encodeSilk(pttPath); + // log("生成语音", silkPath, duration); + if (!silkPath) { + throw '语音转换失败, 请检查语音文件是否正常'; + } + const { md5, fileName, path, fileSize } = await NTQQFileApi.uploadFile(silkPath!, ElementType.PTT); + if (fileSize === 0) { + throw '文件异常,大小为0'; + } + if (converted) { + fs.unlink(silkPath).then(); + } + return { + elementType: ElementType.PTT, + elementId: '', + pttElement: { + fileName: fileName, + filePath: path, + md5HexStr: md5, + fileSize: fileSize, + // duration: Math.max(1, Math.round(fileSize / 1024 / 3)), // 一秒钟大概是3kb大小, 小于1秒的按1秒算 + duration: duration || 1, + formatType: 1, + voiceType: 1, + voiceChangeType: 0, + canConvert2Text: true, + waveAmplitudes: [ + 0, 18, 9, 23, 16, 17, 16, 15, 44, 17, 24, 20, 14, 15, 17, + ], + fileSubId: '', + playState: 1, + autoConvertText: 0, + } + }; + } + // NodeIQQNTWrapperSession sendMsg [ + // "0", + // { + // "peerUid": "u_e_RIxgTs2NaJ68h0PwOPSg", + // "chatType": 1, + // "guildId": "" + // }, + // [ + // { + // "elementId": "0", + // "elementType": 6, + // "faceElement": { + // "faceIndex": 0, + // "faceType": 5, + // "msgType": 0, + // "pokeType": 1, + // "pokeStrength": 0 + // } + // } + // ], + // {} + // ] + static face(CoreContext: NapCatCore, faceId: number): SendFaceElement { + // 从face_config.json中获取表情名称 + const sysFaces = faceConfig.sysface; + const emojiFaces = faceConfig.emoji; + const face: any = sysFaces.find((face) => face.QSid === faceId.toString()); + faceId = parseInt(faceId.toString()); + // let faceType = parseInt(faceId.toString().substring(0, 1)); + let faceType = 1; + if (faceId >= 222) { + faceType = 2; + } + if (face.AniStickerType) { + faceType = 3; + } + return { + elementType: ElementType.FACE, + elementId: '', + faceElement: { + faceIndex: faceId, + faceType, + faceText: face.QDes, + stickerId: face.AniStickerId, + stickerType: face.AniStickerType, + packId: face.AniStickerPackId, + sourceType: 1, + }, + }; + } + + static mface(CoreContext: NapCatCore, emojiPackageId: number, emojiId: string, key: string, faceName: string): SendMarketFaceElement { + return { + elementType: ElementType.MFACE, + marketFaceElement: { + emojiPackageId, + emojiId, + key, + faceName: faceName || '[商城表情]', + }, + }; + } + + static dice(CoreContext: NapCatCore, resultId: number | null): SendFaceElement { + // 实际测试并不能控制结果 + + // 随机1到6 + // if (isNull(resultId)) resultId = Math.floor(Math.random() * 6) + 1; + return { + elementType: ElementType.FACE, + elementId: '', + faceElement: { + faceIndex: FaceIndex.dice, + faceType: FaceType.dice, + 'faceText': '[骰子]', + 'packId': '1', + 'stickerId': '33', + 'sourceType': 1, + 'stickerType': 2, + // resultId: resultId.toString(), + 'surpriseId': '', + // "randomType": 1, + } + }; + } + + // 猜拳(石头剪刀布)表情 + static rps(CoreContext: NapCatCore, resultId: number | null): SendFaceElement { + // 实际测试并不能控制结果 + // if (isNull(resultId)) resultId = Math.floor(Math.random() * 3) + 1; + return { + elementType: ElementType.FACE, + elementId: '', + faceElement: { + 'faceIndex': FaceIndex.RPS, + 'faceText': '[包剪锤]', + 'faceType': 3, + 'packId': '1', + 'stickerId': '34', + 'sourceType': 1, + 'stickerType': 2, + // 'resultId': resultId.toString(), + 'surpriseId': '', + // "randomType": 1, + } + }; + } + + static ark(CoreContext: NapCatCore, data: any): SendArkElement { + if (typeof data !== 'string') { + data = JSON.stringify(data); + } + return { + elementType: ElementType.ARK, + elementId: '', + arkElement: { + bytesData: data, + linkInfo: null, + subElementType: null + } + }; + } + + static markdown(CoreContext: NapCatCore, content: string): SendMarkdownElement { + return { + elementType: ElementType.MARKDOWN, + elementId: '', + markdownElement: { + content + } + }; + } +}