From f42727c8adf5d7556e2afff00f1db738b73d14a0 Mon Sep 17 00:00:00 2001 From: linyuchen Date: Sat, 16 Mar 2024 11:20:00 +0800 Subject: [PATCH 1/5] feat: invite join group event --- src/ntqqapi/types.ts | 12 ++++- src/onebot11/constructor.ts | 54 +++++++++---------- .../event/notice/OB11GroupIncreaseEvent.ts | 7 +-- 3 files changed, 40 insertions(+), 33 deletions(-) diff --git a/src/ntqqapi/types.ts b/src/ntqqapi/types.ts index 743b6df..6618982 100644 --- a/src/ntqqapi/types.ts +++ b/src/ntqqapi/types.ts @@ -178,6 +178,7 @@ export interface SendVideoElement { elementId: "", videoElement: VideoElement } + export interface SendArkElement { elementType: ElementType.ARK, elementId: "", @@ -243,7 +244,12 @@ export interface PicElement { md5HexStr?: string; } +export enum GrayTipElementSubType { + INVITE_NEW_MEMBER = 12, +} + export interface GrayTipElement { + subElementType: GrayTipElementSubType; revokeElement: { operatorRole: string; operatorUid: string; @@ -253,7 +259,10 @@ export interface GrayTipElement { wording: string; // 自定义的撤回提示语 } aioOpGrayTipElement: TipAioOpGrayTipElement, - groupElement: TipGroupElement + groupElement: TipGroupElement, + xmlElement: { + content: string; + } } export interface FaceElement { @@ -398,6 +407,7 @@ export enum GroupNotifyStatus { APPROVE = 2, REJECT = 3 } + export interface GroupNotify { time: number; // 自己添加的字段,时间戳,毫秒, 用于判断收到短时间内收到重复的notify seq: string, // 唯一标识符,转成数字再除以1000应该就是时间戳? diff --git a/src/onebot11/constructor.ts b/src/onebot11/constructor.ts index 02d3b96..c13cc66 100644 --- a/src/onebot11/constructor.ts +++ b/src/onebot11/constructor.ts @@ -11,6 +11,7 @@ import { import { AtType, ChatType, + GrayTipElementSubType, Group, GroupMember, IMAGE_HTTP_HOST, @@ -19,7 +20,7 @@ import { TipGroupElementType, User } from '../ntqqapi/types'; -import {getFriend, getGroup, getGroupMember, selfInfo, tempGroupCodeMap} from '../common/data'; +import {getFriend, getGroupMember, selfInfo, tempGroupCodeMap} from '../common/data'; import {getConfigUtil, log, sleep} from "../common/utils"; import {NTQQApi} from "../ntqqapi/ntcall"; import {EventType} from "./event/OB11BaseEvent"; @@ -206,33 +207,6 @@ export class OB11Constructor { message_data["type"] = OB11MessageDataType.face; message_data["data"]["id"] = element.faceElement.faceIndex.toString(); } - // todo: 解析入群grayTipElement - else if (element.grayTipElement?.aioOpGrayTipElement) { - log("收到 group gray tip 消息", element.grayTipElement.aioOpGrayTipElement) - } - // if (message_data.data.file) { - // let filePath: string = message_data.data.file; - // if (!enableLocalFile2Url) { - // message_data.data.file = "file://" + filePath - // } else { // 不使用本地路径 - // const ignoreTypes = [OB11MessageDataType.file, OB11MessageDataType.video] - // if (!ignoreTypes.includes(message_data.type)) { - // if (message_data.data.url && !message_data.data.url.startsWith(IMAGE_HTTP_HOST + "/download")) { - // message_data.data.file = message_data.data.url - // } else { - // let { err, data } = await file2base64(filePath); - // if (err) { - // log("文件转base64失败", filePath, err) - // } else { - // message_data.data.file = "base64://" + data - // } - // } - // } else { - // message_data.data.file = "file://" + filePath - // } - // } - // } - if (message_data.type !== "unknown" && message_data.data) { const cqCode = encodeCQCode(message_data); if (messagePostFormat === 'string') { @@ -251,7 +225,8 @@ export class OB11Constructor { return; } for (let element of msg.elements) { - const groupElement = element.grayTipElement?.groupElement + const grayTipElement = element.grayTipElement + const groupElement = grayTipElement?.groupElement if (groupElement) { // log("收到群提示消息", groupElement) if (groupElement.type == TipGroupElementType.memberIncrease) { @@ -297,6 +272,27 @@ export class OB11Constructor { else if (element.fileElement){ return new OB11GroupUploadNoticeEvent(parseInt(msg.peerUid), parseInt(msg.senderUin), {id: element.fileElement.fileName, name: element.fileElement.fileName, size: parseInt(element.fileElement.fileSize)}) } + + if (grayTipElement) { + if (grayTipElement.subElementType == GrayTipElementSubType.INVITE_NEW_MEMBER){ + log("收到新人被邀请进群消息", grayTipElement) + const xmlElement = grayTipElement.xmlElement + if (xmlElement?.content){ + const regex = /jp="(\d+)"/g; + + let matches = []; + let match = null + + while ((match = regex.exec(xmlElement.content)) !== null) { + matches.push(match[1]); + } + if (matches.length === 2){ + const [inviter, invitee] = matches; + return new OB11GroupIncreaseEvent(parseInt(msg.peerUid), parseInt(invitee), parseInt(inviter), "invite"); + } + } + } + } } } diff --git a/src/onebot11/event/notice/OB11GroupIncreaseEvent.ts b/src/onebot11/event/notice/OB11GroupIncreaseEvent.ts index d7e9158..3527820 100644 --- a/src/onebot11/event/notice/OB11GroupIncreaseEvent.ts +++ b/src/onebot11/event/notice/OB11GroupIncreaseEvent.ts @@ -1,14 +1,15 @@ import {OB11GroupNoticeEvent} from "./OB11GroupNoticeEvent"; +type GroupIncreaseSubType = "approve" | "invite"; export class OB11GroupIncreaseEvent extends OB11GroupNoticeEvent { notice_type = "group_increase"; - sub_type = "approve"; // TODO: 实现其他几种子类型的识别 ("approve" | "invite") operator_id: number; - - constructor(groupId: number, userId: number, operatorId: number) { + sub_type: GroupIncreaseSubType; + constructor(groupId: number, userId: number, operatorId: number, subType: GroupIncreaseSubType = "approve") { super(); this.group_id = groupId; this.operator_id = operatorId; this.user_id = userId; + this.sub_type = subType } } From c313fcd4915d1ee947bb885eb5279050231f6c28 Mon Sep 17 00:00:00 2001 From: linyuchen Date: Sat, 16 Mar 2024 12:34:00 +0800 Subject: [PATCH 2/5] refactor: output log ignore ntqq logger event --- manifest.json | 4 ++-- src/common/config.ts | 2 +- src/ntqqapi/hook.ts | 18 +++++++++++++++--- src/onebot11/constructor.ts | 2 +- src/version.ts | 2 +- 5 files changed, 20 insertions(+), 8 deletions(-) diff --git a/manifest.json b/manifest.json index 31edae9..51f84c8 100644 --- a/manifest.json +++ b/manifest.json @@ -1,10 +1,10 @@ { "manifest_version": 4, "type": "extension", - "name": "LLOneBot v3.15.1", + "name": "LLOneBot v3.15.2", "slug": "LLOneBot", "description": "LiteLoaderQQNT的OneBotApi,不支持商店在线更新", - "version": "3.15.1", + "version": "3.15.2", "icon": "./icon.jpg", "authors": [ { diff --git a/src/common/config.ts b/src/common/config.ts index 397c77c..5a7a964 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -2,7 +2,7 @@ import fs from "fs"; import {Config, OB11Config} from './types'; import {mergeNewProperties} from "./utils"; -export const HOOK_LOG = false; +export const HOOK_LOG = true; export const ALLOW_SEND_TEMP_MSG = false; diff --git a/src/ntqqapi/hook.ts b/src/ntqqapi/hook.ts index c50f6d7..3ab62b1 100644 --- a/src/ntqqapi/hook.ts +++ b/src/ntqqapi/hook.ts @@ -54,7 +54,13 @@ let receiveHooks: Array<{ export function hookNTQQApiReceive(window: BrowserWindow) { const originalSend = window.webContents.send; const patchSend = (channel: string, ...args: NTQQApiReturnData) => { - HOOK_LOG && log(`received ntqq api message: ${channel}`, JSON.stringify(args)) + try { + if (!args[0]?.eventName?.startsWith("ns-LoggerApi")) { + HOOK_LOG && log(`received ntqq api message: ${channel}`, JSON.stringify(args)) + } + } catch (e) { + + } if (args?.[1] instanceof Array) { for (let receiveData of args?.[1]) { const ntQQApiMethodName = receiveData.cmdName; @@ -98,7 +104,13 @@ export function hookNTQQApiCall(window: BrowserWindow) { const proxyIpcMsg = new Proxy(ipc_message_proxy, { apply(target, thisArg, args) { - HOOK_LOG && log("call NTQQ api", thisArg, args); + try { + if (args[3][1][0] !== "info") { + HOOK_LOG && log("call NTQQ api", thisArg, args); + } + } catch (e) { + + } return target.apply(thisArg, args); }, }); @@ -241,7 +253,7 @@ registerReceiveHook<{ msgList: Array }>(ReceiveCmd.NEW_MSG, (payload pathList.push(...Object.values(msgElement.picElement.thumbPath)) } const aioOpGrayTipElement = msgElement.grayTipElement?.aioOpGrayTipElement - if (aioOpGrayTipElement){ + if (aioOpGrayTipElement) { tempGroupCodeMap[aioOpGrayTipElement.peerUid] = aioOpGrayTipElement.fromGrpCodeOfTmpChat; } diff --git a/src/onebot11/constructor.ts b/src/onebot11/constructor.ts index c13cc66..d55c5ce 100644 --- a/src/onebot11/constructor.ts +++ b/src/onebot11/constructor.ts @@ -270,7 +270,7 @@ export class OB11Constructor { } } else if (element.fileElement){ - return new OB11GroupUploadNoticeEvent(parseInt(msg.peerUid), parseInt(msg.senderUin), {id: element.fileElement.fileName, name: element.fileElement.fileName, size: parseInt(element.fileElement.fileSize)}) + return new OB11GroupUploadNoticeEvent(parseInt(msg.peerUid), parseInt(msg.senderUin), {id: element.fileElement.fileUuid, name: element.fileElement.fileName, size: parseInt(element.fileElement.fileSize)}) } if (grayTipElement) { diff --git a/src/version.ts b/src/version.ts index 2e68519..fe899f1 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const version = "3.15.1" \ No newline at end of file +export const version = "3.15.2" \ No newline at end of file From d8e31985afffbd7c0efb88b9741f248593bd8e5c Mon Sep 17 00:00:00 2001 From: linyuchen Date: Sun, 17 Mar 2024 09:07:33 +0800 Subject: [PATCH 3/5] refactor: class NTQQApi --- manifest.json | 4 +- src/common/data.ts | 12 +- src/common/utils/index.ts | 342 ++++++++ src/common/utils/qqpkg.ts | 10 + src/main/main.ts | 29 +- src/ntqqapi/api/file.ts | 217 +++++ src/ntqqapi/api/friend.ts | 61 ++ src/ntqqapi/api/group.ts | 223 ++++++ src/ntqqapi/api/msg.ts | 163 ++++ src/ntqqapi/api/user.ts | 57 ++ src/ntqqapi/constructor.ts | 12 +- src/ntqqapi/hook.ts | 66 +- src/ntqqapi/ntcall.ts | 742 +----------------- src/onebot11/action/CleanCache.ts | 20 +- src/onebot11/action/DeleteMsg.ts | 4 +- src/onebot11/action/GetGroupMemberList.ts | 4 +- src/onebot11/action/SendLike.ts | 4 +- src/onebot11/action/SendMsg.ts | 9 +- src/onebot11/action/SetFriendAddRequest.ts | 4 +- src/onebot11/action/SetGroupAddRequest.ts | 4 +- src/onebot11/action/SetGroupAdmin.ts | 4 +- src/onebot11/action/SetGroupBan.ts | 4 +- src/onebot11/action/SetGroupCard.ts | 4 +- src/onebot11/action/SetGroupKick.ts | 4 +- src/onebot11/action/SetGroupLeave.ts | 4 +- src/onebot11/action/SetGroupName.ts | 4 +- src/onebot11/action/SetGroupWholeBan.ts | 4 +- .../action/go-cqhttp/UploadGroupFile.ts | 4 +- .../action/llonebot/GetGroupAddRequest.ts | 7 +- src/onebot11/action/llonebot/SetQQAvatar.ts | 4 +- src/onebot11/constructor.ts | 15 +- src/version.ts | 2 +- 32 files changed, 1199 insertions(+), 848 deletions(-) create mode 100644 src/common/utils/index.ts create mode 100644 src/common/utils/qqpkg.ts create mode 100644 src/ntqqapi/api/file.ts create mode 100644 src/ntqqapi/api/friend.ts create mode 100644 src/ntqqapi/api/group.ts create mode 100644 src/ntqqapi/api/msg.ts create mode 100644 src/ntqqapi/api/user.ts diff --git a/manifest.json b/manifest.json index 51f84c8..9c887d8 100644 --- a/manifest.json +++ b/manifest.json @@ -1,10 +1,10 @@ { "manifest_version": 4, "type": "extension", - "name": "LLOneBot v3.15.2", + "name": "LLOneBot v3.15.3", "slug": "LLOneBot", "description": "LiteLoaderQQNT的OneBotApi,不支持商店在线更新", - "version": "3.15.2", + "version": "3.15.3", "icon": "./icon.jpg", "authors": [ { diff --git a/src/common/data.ts b/src/common/data.ts index b47d7c9..d342124 100644 --- a/src/common/data.ts +++ b/src/common/data.ts @@ -1,17 +1,13 @@ -import {NTQQApi} from '../ntqqapi/ntcall' import { type Friend, type FriendRequest, type Group, type GroupMember, - type GroupNotify, - type RawMessage, type SelfInfo } from '../ntqqapi/types' import {type FileCache, type LLOneBotError} from './types' -import {dbUtil} from "./db"; -import {raw} from "express"; import {isNumeric, log} from "./utils"; +import {NTQQGroupApi} from "../ntqqapi/api/group"; export const selfInfo: SelfInfo = { uid: '', @@ -47,7 +43,7 @@ export async function getGroup(qq: string): Promise { let group = groups.find(group => group.groupCode === qq.toString()) if (!group) { try { - const _groups = await NTQQApi.getGroups(true); + const _groups = await NTQQGroupApi.getGroups(true); group = _groups.find(group => group.groupCode === qq.toString()) if (group) { groups.push(group) @@ -70,7 +66,7 @@ export async function getGroupMember(groupQQ: string | number, memberUinOrUid: s let member = group.members?.find(filterFunc) if (!member) { try { - const _members = await NTQQApi.getGroupMembers(groupQQ) + const _members = await NTQQGroupApi.getGroupMembers(groupQQ) if (_members.length > 0) { group.members = _members } @@ -88,7 +84,7 @@ export async function getGroupMember(groupQQ: string | number, memberUinOrUid: s export async function refreshGroupMembers(groupQQ: string) { const group = groups.find(group => group.groupCode === groupQQ) if (group) { - group.members = await NTQQApi.getGroupMembers(groupQQ) + group.members = await NTQQGroupApi.getGroupMembers(groupQQ) } } diff --git a/src/common/utils/index.ts b/src/common/utils/index.ts new file mode 100644 index 0000000..b9153ed --- /dev/null +++ b/src/common/utils/index.ts @@ -0,0 +1,342 @@ +import * as path from "node:path"; +import {selfInfo} from "../data"; +import {ConfigUtil} from "../config"; +import util from "util"; +import {encode, getDuration, isWav} from "silk-wasm"; +import fs from 'fs'; +import * as crypto from 'crypto'; +import {v4 as uuidv4} from "uuid"; +import ffmpeg from "fluent-ffmpeg" + +export const DATA_DIR = global.LiteLoader.plugins["LLOneBot"].path.data; + +export function getConfigUtil() { + const configFilePath = path.join(DATA_DIR, `config_${selfInfo.uin}.json`) + return new ConfigUtil(configFilePath) +} + +function truncateString(obj: any, maxLength = 500) { + if (obj !== null && typeof obj === 'object') { + Object.keys(obj).forEach(key => { + if (typeof obj[key] === 'string') { + // 如果是字符串且超过指定长度,则截断 + if (obj[key].length > maxLength) { + obj[key] = obj[key].substring(0, maxLength) + '...'; + } + } else if (typeof obj[key] === 'object') { + // 如果是对象或数组,则递归调用 + truncateString(obj[key], maxLength); + } + }); + } + return obj; +} + +export function isNumeric(str: string) { + return /^\d+$/.test(str); +} + + +export function log(...msg: any[]) { + if (!getConfigUtil().getConfig().log) { + return //console.log(...msg); + } + let currentDateTime = new Date().toLocaleString(); + const date = new Date(); + const year = date.getFullYear(); + const month = date.getMonth() + 1; + const day = date.getDate(); + const currentDate = `${year}-${month}-${day}`; + const userInfo = selfInfo.uin ? `${selfInfo.nick}(${selfInfo.uin})` : "" + let logMsg = ""; + for (let msgItem of msg) { + // 判断是否是对象 + if (typeof msgItem === "object") { + let obj = JSON.parse(JSON.stringify(msgItem)); + logMsg += JSON.stringify(truncateString(obj)) + " "; + continue; + } + logMsg += msgItem + " "; + } + logMsg = `${currentDateTime} ${userInfo}: ${logMsg}\n\n` + // sendLog(...msg); + // console.log(msg) + fs.appendFile(path.join(DATA_DIR, `llonebot-${currentDate}.log`), logMsg, (err: any) => { + + }) +} + +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 sleep(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); +} + + +// 定义一个异步函数来检查文件是否存在 +export function checkFileReceived(path: string, timeout: number = 3000): Promise { + 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 file2base64(path: string) { + const readFile = util.promisify(fs.readFile); + let 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) { + result.err = err.toString(); + } + return result; +} + + +// 在保证老对象已有的属性不变化的情况下将新对象的属性复制到老对象 +export function mergeNewProperties(newObj: any, oldObj: any) { + Object.keys(newObj).forEach(key => { + // 如果老对象不存在当前属性,则直接复制 + if (!oldObj.hasOwnProperty(key)) { + oldObj[key] = newObj[key]; + } else { + // 如果老对象和新对象的当前属性都是对象,则递归合并 + if (typeof oldObj[key] === 'object' && typeof newObj[key] === 'object') { + mergeNewProperties(newObj[key], oldObj[key]); + } else if (typeof oldObj[key] === 'object' || typeof newObj[key] === 'object') { + // 属性冲突,有一方不是对象,直接覆盖 + oldObj[key] = newObj[key]; + } + } + }); +} + +export function checkFfmpeg(newPath: string = null): Promise { + return new Promise((resolve, reject) => { + if (newPath) { + ffmpeg.setFfmpegPath(newPath); + ffmpeg.getAvailableFormats((err, formats) => { + if (err) { + log('ffmpeg is not installed or not found in PATH:', err); + resolve(false) + } else { + log('ffmpeg is installed.'); + resolve(true); + } + }) + } + }); +} + +export async function encodeSilk(filePath: string) { + const fsp = require("fs").promises + + function getFileHeader(filePath: string) { + // 定义要读取的字节数 + const bytesToRead = 7; + try { + const buffer = fs.readFileSync(filePath, { + encoding: null, + flag: "r", + }); + + const fileHeader = buffer.toString("hex", 0, bytesToRead); + return fileHeader; + } catch (err) { + console.error("读取文件错误:", err); + return; + } + } + + async function isWavFile(filePath: string) { + return isWav(fs.readFileSync(filePath)); + } + + // async function getAudioSampleRate(filePath: string) { + // try { + // const mm = await import('music-metadata'); + // const metadata = await mm.parseFile(filePath); + // log(`${filePath}采样率`, metadata.format.sampleRate); + // return metadata.format.sampleRate; + // } catch (error) { + // log(`${filePath}采样率获取失败`, error.stack); + // // console.error(error); + // } + // } + + try { + const fileName = path.basename(filePath); + const pttPath = path.join(DATA_DIR, uuidv4()); + if (getFileHeader(filePath) !== "02232153494c4b") { + log(`语音文件${filePath}需要转换成silk`) + const _isWav = await isWavFile(filePath); + const wavPath = pttPath + ".wav" + if (!_isWav) { + log(`语音文件${filePath}正在转换成wav`) + // let voiceData = await fsp.readFile(filePath) + await new Promise((resolve, reject) => { + const ffmpegPath = getConfigUtil().getConfig().ffmpeg; + if (ffmpegPath) { + ffmpeg.setFfmpegPath(ffmpegPath); + } + ffmpeg(filePath).toFormat("wav").audioChannels(2).on('end', function () { + log('wav转换完成'); + }) + .on('error', function (err) { + log(`wav转换出错: `, err.message,); + reject(err); + }) + .save(wavPath) + .on("end", () => { + filePath = wavPath + resolve(wavPath); + }); + }) + } + // const sampleRate = await getAudioSampleRate(filePath) || 0; + // log("音频采样率", sampleRate) + const pcm = fs.readFileSync(filePath); + const silk = await encode(pcm, 0); + fs.writeFileSync(pttPath, silk.data); + fs.unlink(wavPath, (err) => { }); + log(`语音文件${filePath}转换成功!`, pttPath) + return { + converted: true, + path: pttPath, + duration: silk.duration, + }; + } else { + const pcm = fs.readFileSync(filePath); + let duration = 0; + try { + duration = getDuration(pcm); + } catch (e) { + log("获取语音文件时长失败", filePath, e.stack) + duration = fs.statSync(filePath).size / 1024 / 3 // 每3kb大约1s + duration = Math.floor(duration) + duration = Math.max(1, duration) + log("使用文件大小估算时长", duration) + } + + return { + converted: false, + path: filePath, + duration: duration, + }; + } + } catch (error) { + log("convert silk failed", error.stack); + return {}; + } +} + +export async function getVideoInfo(filePath: string) { + const size = fs.statSync(filePath).size; + return new Promise<{ width: number, height: number, time: number, format: string, size: number, filePath: string }>((resolve, reject) => { + ffmpeg(filePath).ffprobe( (err, metadata) => { + if (err) { + reject(err); + } else { + const videoStream = metadata.streams.find(s => s.codec_type === 'video'); + if (videoStream) { + console.log(`视频尺寸: ${videoStream.width}x${videoStream.height}`); + } else { + console.log('未找到视频流信息。'); + } + resolve({ + width: videoStream.width, height: videoStream.height, + time: parseInt(videoStream.duration), + format: metadata.format.format_name, + size, + filePath + }); + } + }); + }) +} + + +export async function encodeMp4(filePath: string) { + let videoInfo = await getVideoInfo(filePath); + log("视频信息", videoInfo) + if (videoInfo.format.indexOf("mp4") === -1) { + log("视频需要转换为MP4格式", filePath) + // 转成mp4 + const newPath: string = await new Promise((resolve, reject) => { + const newPath = filePath + ".mp4" + ffmpeg(filePath) + .toFormat('mp4') + .on('error', (err) => { + reject(`转换视频格式失败: ${err.message}`); + }) + .on('end', () => { + log('视频转换为MP4格式完成'); + resolve(newPath); // 返回转换后的文件路径 + }) + .save(newPath); + }); + return await getVideoInfo(newPath) + } + return videoInfo +} + +export function isNull(value: any) { + return value === undefined || value === null; +} + + +export function calculateFileMD5(filePath: string): Promise { + 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); + }); + }); +} diff --git a/src/common/utils/qqpkg.ts b/src/common/utils/qqpkg.ts new file mode 100644 index 0000000..5a407c6 --- /dev/null +++ b/src/common/utils/qqpkg.ts @@ -0,0 +1,10 @@ +import path from "path"; + +type QQPkgInfo = { + version: string; + buildVersion: string; + platform: string; + eleArch: string; +} + +export const qqPkgInfo: QQPkgInfo = require(path.join(process.resourcesPath, "app/package.json")) diff --git a/src/main/main.ts b/src/main/main.ts index fc769da..06b4091 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -21,9 +21,8 @@ import { refreshGroupMembers, selfInfo } from "../common/data"; -import {hookNTQQApiCall, hookNTQQApiReceive, ReceiveCmd, registerReceiveHook} from "../ntqqapi/hook"; +import {hookNTQQApiCall, hookNTQQApiReceive, ReceiveCmdS, registerReceiveHook} from "../ntqqapi/hook"; import {OB11Constructor} from "../onebot11/constructor"; -import {NTQQApi} from "../ntqqapi/ntcall"; import {ChatType, FriendRequestNotify, GroupNotifies, GroupNotifyTypes, RawMessage} from "../ntqqapi/types"; import {ob11HTTPServer} from "../onebot11/server/http"; import {OB11FriendRecallNoticeEvent} from "../onebot11/event/notice/OB11FriendRecallNoticeEvent"; @@ -36,6 +35,8 @@ import {OB11FriendRequestEvent} from "../onebot11/event/request/OB11FriendReques import * as path from "node:path"; import {dbUtil} from "../common/db"; import {setConfig} from "./setConfig"; +import {NTQQUserApi} from "../ntqqapi/api/user"; +import {NTQQGroupApi} from "../ntqqapi/api/group"; let running = false; @@ -123,14 +124,14 @@ function onLoad() { } async function startReceiveHook() { - registerReceiveHook<{ msgList: Array }>(ReceiveCmd.NEW_MSG, async (payload) => { + registerReceiveHook<{ msgList: Array }>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], async (payload) => { try { await postReceiveMsg(payload.msgList); } catch (e) { log("report message error: ", e.stack.toString()); } }) - registerReceiveHook<{ msgList: Array }>(ReceiveCmd.UPDATE_MSG, async (payload) => { + registerReceiveHook<{ msgList: Array }>([ReceiveCmdS.UPDATE_MSG, ReceiveCmdS.UPDATE_ACTIVE_MSG], async (payload) => { for (const message of payload.msgList) { // log("message update", message.sendStatus, message.msgId, message.msgSeq) if (message.recallTime != "0") { //todo: 这个判断方法不太好,应该使用灰色消息元素来判断 @@ -166,7 +167,7 @@ function onLoad() { dbUtil.updateMsg(message).then(); } }) - registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmd.SELF_SEND_MSG, async (payload) => { + registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, async (payload) => { const {reportSelfMessage} = getConfigUtil().getConfig(); if (!reportSelfMessage) { return @@ -182,12 +183,12 @@ function onLoad() { "doubt": boolean, "oldestUnreadSeq": string, "unreadCount": number - }>(ReceiveCmd.UNREAD_GROUP_NOTIFY, async (payload) => { + }>(ReceiveCmdS.UNREAD_GROUP_NOTIFY, async (payload) => { if (payload.unreadCount) { // log("开始获取群通知详情") let notify: GroupNotifies; try { - notify = await NTQQApi.getGroupNotifies(); + notify = await NTQQGroupApi.getGroupNotifies(); } catch (e) { // log("获取群通知详情失败", e); return @@ -240,7 +241,7 @@ function onLoad() { groupRequestEvent.group_id = parseInt(notify.group.groupCode); let requestQQ = "" try { - requestQQ = (await NTQQApi.getUserDetailInfo(notify.user1.uid)).uin; + requestQQ = (await NTQQUserApi.getUserDetailInfo(notify.user1.uid)).uin; } catch (e) { log("获取加群人QQ号失败", e) } @@ -255,7 +256,7 @@ function onLoad() { groupInviteEvent.group_id = parseInt(notify.group.groupCode); let user_id = (await getFriend(notify.user2.uid))?.uin if (!user_id) { - user_id = (await NTQQApi.getUserDetailInfo(notify.user2.uid))?.uin + user_id = (await NTQQUserApi.getUserDetailInfo(notify.user2.uid))?.uin } groupInviteEvent.user_id = parseInt(user_id); groupInviteEvent.sub_type = "invite"; @@ -272,14 +273,14 @@ function onLoad() { } }) - registerReceiveHook(ReceiveCmd.FRIEND_REQUEST, async (payload) => { + registerReceiveHook(ReceiveCmdS.FRIEND_REQUEST, async (payload) => { for (const req of payload.data.buddyReqs) { if (req.isUnread && !friendRequests[req.sourceId] && (parseInt(req.reqTime) > startTime / 1000)) { friendRequests[req.sourceId] = req; log("有新的好友请求", req); let friendRequestEvent = new OB11FriendRequestEvent(); try { - let requester = await NTQQApi.getUserDetailInfo(req.friendUid) + let requester = await NTQQUserApi.getUserDetailInfo(req.friendUid) friendRequestEvent.user_id = parseInt(requester.uin); } catch (e) { log("获取加好友者QQ号失败", e); @@ -298,7 +299,7 @@ function onLoad() { log("llonebot pid", process.pid) startTime = Date.now(); startReceiveHook().then(); - NTQQApi.getGroups(true).then() + NTQQGroupApi.getGroups(true).then() const config = getConfigUtil().getConfig() // 检查ffmpeg checkFfmpeg(config.ffmpeg).then(exist => { @@ -327,7 +328,7 @@ function onLoad() { const init = async () => { try { log("start get self info") - const _ = await NTQQApi.getSelfInfo(); + const _ = await NTQQUserApi.getSelfInfo(); log("get self info api result:", _); Object.assign(selfInfo, _); selfInfo.nick = selfInfo.uin; @@ -337,7 +338,7 @@ function onLoad() { log("self info", selfInfo); if (selfInfo.uin) { try { - const userInfo = (await NTQQApi.getUserDetailInfo(selfInfo.uid)); + const userInfo = (await NTQQUserApi.getUserDetailInfo(selfInfo.uid)); log("self info", userInfo); if (userInfo) { selfInfo.nick = userInfo.nick; diff --git a/src/ntqqapi/api/file.ts b/src/ntqqapi/api/file.ts new file mode 100644 index 0000000..1f1e811 --- /dev/null +++ b/src/ntqqapi/api/file.ts @@ -0,0 +1,217 @@ +import {callNTQQApi, GeneralCallResult, NTQQApiClass, NTQQApiMethod} from "../ntcall"; +import { + CacheFileList, + CacheFileListItem, + CacheFileType, + CacheScanResult, + ChatCacheList, ChatCacheListItemBasic, + ChatType, + ElementType +} from "../types"; +import path from "path"; +import {log} from "../../common/utils"; +import fs from "fs"; +import {ReceiveCmdS} from "../hook"; + +export class NTQQFileApi{ + static async getFileType(filePath: string) { + return await callNTQQApi<{ ext: string }>({ + className: NTQQApiClass.FS_API, methodName: NTQQApiMethod.FILE_TYPE, args: [filePath] + }) + } + static async getFileMd5(filePath: string) { + return await callNTQQApi({ + className: NTQQApiClass.FS_API, + methodName: NTQQApiMethod.FILE_MD5, + args: [filePath] + }) + } + static async copyFile(filePath: string, destPath: string) { + return await callNTQQApi({ + className: NTQQApiClass.FS_API, + methodName: NTQQApiMethod.FILE_COPY, + args: [{ + fromPath: filePath, + toPath: destPath + }] + }) + } + static async getFileSize(filePath: string) { + return await callNTQQApi({ + className: NTQQApiClass.FS_API, methodName: NTQQApiMethod.FILE_SIZE, args: [filePath] + }) + } + // 上传文件到QQ的文件夹 + static async uploadFile(filePath: string, elementType: ElementType = ElementType.PIC) { + const md5 = await NTQQFileApi.getFileMd5(filePath); + let ext = (await NTQQFileApi.getFileType(filePath))?.ext + if (ext) { + ext = "." + ext + } else { + ext = "" + } + let fileName = `${path.basename(filePath)}`; + if (fileName.indexOf(".") === -1) { + fileName += ext; + } + const mediaPath = await callNTQQApi({ + methodName: NTQQApiMethod.MEDIA_FILE_PATH, + args: [{ + path_info: { + md5HexStr: md5, + fileName: fileName, + elementType: elementType, + elementSubType: 0, + thumbSize: 0, + needCreate: true, + downloadType: 1, + file_uuid: "" + } + }] + }) + log("media path", mediaPath) + await NTQQFileApi.copyFile(filePath, mediaPath); + const fileSize = await NTQQFileApi.getFileSize(filePath); + return { + md5, + fileName, + path: mediaPath, + fileSize + } + } + static async downloadMedia(msgId: string, chatType: ChatType, peerUid: string, elementId: string, thumbPath: string, sourcePath: string) { + // 用于下载收到的消息中的图片等 + if (fs.existsSync(sourcePath)) { + return sourcePath + } + const apiParams = [ + { + getReq: { + msgId: msgId, + chatType: chatType, + peerUid: peerUid, + elementId: elementId, + thumbSize: 0, + downloadType: 1, + filePath: thumbPath, + }, + }, + undefined, + ] + // log("需要下载media", sourcePath); + await callNTQQApi({ + methodName: NTQQApiMethod.DOWNLOAD_MEDIA, + args: apiParams, + cbCmd: ReceiveCmdS.MEDIA_DOWNLOAD_COMPLETE, + cmdCB: (payload: { notifyInfo: { filePath: string } }) => { + // log("media 下载完成判断", payload.notifyInfo.filePath, sourcePath); + return payload.notifyInfo.filePath == sourcePath; + } + }) + return sourcePath + } + static async getImageSize(filePath: string) { + return await callNTQQApi<{ width: number, height: number }>({ + className: NTQQApiClass.FS_API, methodName: NTQQApiMethod.IMAGE_SIZE, args: [filePath] + }) + } + +} + +export class NTQQFileCacheApi{ + static async setCacheSilentScan(isSilent: boolean = true) { + return await callNTQQApi({ + methodName: NTQQApiMethod.CACHE_SET_SILENCE, + args: [{ + isSilent + }, null] + }); + } + static getCacheSessionPathList() { + return callNTQQApi<{ + key: string, + value: string + }[]>({ + className: NTQQApiClass.OS_API, + methodName: NTQQApiMethod.CACHE_PATH_SESSION, + }); + } + static clearCache(cacheKeys: Array = ['tmp', 'hotUpdate']) { + return callNTQQApi({ // TODO: 目前还不知道真正的返回值是什么 + methodName: NTQQApiMethod.CACHE_CLEAR, + args: [{ + keys: cacheKeys + }, null] + }); + } + static addCacheScannedPaths(pathMap: object = {}) { + return callNTQQApi({ + methodName: NTQQApiMethod.CACHE_ADD_SCANNED_PATH, + args: [{ + pathMap: {...pathMap}, + }, null] + }); + } + static scanCache() { + callNTQQApi({ + methodName: ReceiveCmdS.CACHE_SCAN_FINISH, + classNameIsRegister: true, + }).then(); + return callNTQQApi({ + methodName: NTQQApiMethod.CACHE_SCAN, + args: [null, null], + timeoutSecond: 300, + }); + } + static getHotUpdateCachePath() { + return callNTQQApi({ + className: NTQQApiClass.HOTUPDATE_API, + methodName: NTQQApiMethod.CACHE_PATH_HOT_UPDATE + }); + } + + static getDesktopTmpPath() { + return callNTQQApi({ + className: NTQQApiClass.BUSINESS_API, + methodName: NTQQApiMethod.CACHE_PATH_DESKTOP_TEMP + }); + } + static getChatCacheList(type: ChatType, pageSize: number = 1000, pageIndex: number = 0) { + return new Promise((res, rej) => { + callNTQQApi({ + methodName: NTQQApiMethod.CACHE_CHAT_GET, + args: [{ + chatType: type, + pageSize, + order: 1, + pageIndex + }, null] + }).then(list => res(list)) + .catch(e => rej(e)); + }); + } + static getFileCacheInfo(fileType: CacheFileType, pageSize: number = 1000, lastRecord?: CacheFileListItem) { + const _lastRecord = lastRecord ? lastRecord : {fileType: fileType}; + + return callNTQQApi({ + methodName: NTQQApiMethod.CACHE_FILE_GET, + args: [{ + fileType: fileType, + restart: true, + pageSize: pageSize, + order: 1, + lastRecord: _lastRecord, + }, null] + }) + } + static async clearChatCache(chats: ChatCacheListItemBasic[] = [], fileKeys: string[] = []) { + return await callNTQQApi({ + methodName: NTQQApiMethod.CACHE_CHAT_CLEAR, + args: [{ + chats, + fileKeys + }, null] + }); + } + +} \ No newline at end of file diff --git a/src/ntqqapi/api/friend.ts b/src/ntqqapi/api/friend.ts new file mode 100644 index 0000000..36bfc33 --- /dev/null +++ b/src/ntqqapi/api/friend.ts @@ -0,0 +1,61 @@ +import {Friend, FriendRequest} from "../types"; +import {ReceiveCmdS} from "../hook"; +import {callNTQQApi, GeneralCallResult, NTQQApiMethod} from "../ntcall"; +import {friendRequests} from "../../common/data"; + +export class NTQQFriendApi{ + static async getFriends(forced = false) { + const data = await callNTQQApi<{ + data: { + categoryId: number, + categroyName: string, + categroyMbCount: number, + buddyList: Friend[] + }[] + }>( + { + methodName: NTQQApiMethod.FRIENDS, + args: [{force_update: forced}, undefined], + cbCmd: ReceiveCmdS.FRIENDS + }) + let _friends: Friend[] = []; + for (const fData of data.data) { + _friends.push(...fData.buddyList) + } + return _friends + } + static async likeFriend(uid: string, count = 1) { + return await callNTQQApi({ + methodName: NTQQApiMethod.LIKE_FRIEND, + args: [{ + doLikeUserInfo: { + friendUid: uid, + sourceId: 71, + doLikeCount: count, + doLikeTollCount: 0 + } + }, null] + }) + } + static async handleFriendRequest(sourceId: number, accept: boolean,) { + const request: FriendRequest = friendRequests[sourceId] + if (!request) { + throw `sourceId ${sourceId}, 对应的好友请求不存在` + } + const result = await callNTQQApi({ + methodName: NTQQApiMethod.HANDLE_FRIEND_REQUEST, + args: [ + { + "approvalInfo": { + "friendUid": request.friendUid, + "reqTime": request.reqTime, + accept + } + } + ] + }) + delete friendRequests[sourceId]; + return result; + } + +} \ No newline at end of file diff --git a/src/ntqqapi/api/group.ts b/src/ntqqapi/api/group.ts new file mode 100644 index 0000000..c7a63f6 --- /dev/null +++ b/src/ntqqapi/api/group.ts @@ -0,0 +1,223 @@ +import {ReceiveCmdS} from "../hook"; +import {Group, GroupMember, GroupMemberRole, GroupNotifies, GroupNotify, GroupRequestOperateTypes} from "../types"; +import {callNTQQApi, GeneralCallResult, NTQQApiClass, NTQQApiMethod} from "../ntcall"; +import {uidMaps} from "../../common/data"; +import {log} from "../../common/utils"; +import {BrowserWindow} from "electron"; +import {dbUtil} from "../../common/db"; + +export class NTQQGroupApi{ + static async getGroups(forced = false) { + let cbCmd = ReceiveCmdS.GROUPS + if (process.platform != "win32") { + cbCmd = ReceiveCmdS.GROUPS_UNIX + } + const result = await callNTQQApi<{ + updateType: number, + groupList: Group[] + }>({methodName: NTQQApiMethod.GROUPS, args: [{force_update: forced}, undefined], cbCmd}) + return result.groupList + } + static async getGroupMembers(groupQQ: string, num = 3000): Promise { + const sceneId = await callNTQQApi({ + methodName: NTQQApiMethod.GROUP_MEMBER_SCENE, + args: [{ + groupCode: groupQQ, + scene: "groupMemberList_MainWindow" + }] + }) + // log("get group member sceneId", sceneId); + try { + const result = await callNTQQApi<{ + result: { infos: any } + }>({ + methodName: NTQQApiMethod.GROUP_MEMBERS, + args: [{ + sceneId: sceneId, + num: num + }, + null + ] + }) + // log("members info", typeof result.result.infos, Object.keys(result.result.infos)) + const values = result.result.infos.values() + + const members: GroupMember[] = Array.from(values) + for (const member of members) { + uidMaps[member.uid] = member.uin; + } + // log(uidMaps); + // log("members info", values); + log(`get group ${groupQQ} members success`) + return members + } catch (e) { + log(`get group ${groupQQ} members failed`, e) + return [] + } + } + static async getGroupNotifies() { + // 获取管理员变更 + // 加群通知,退出通知,需要管理员权限 + callNTQQApi({ + methodName: ReceiveCmdS.GROUP_NOTIFY, + classNameIsRegister: true, + }).then() + return await callNTQQApi({ + methodName: NTQQApiMethod.GET_GROUP_NOTICE, + cbCmd: ReceiveCmdS.GROUP_NOTIFY, + afterFirstCmd: false, + args: [ + {"doubt": false, "startSeq": "", "number": 14}, + null + ] + }); + } + static async getGroupIgnoreNotifies() { + await NTQQGroupApi.getGroupNotifies(); + const result = callNTQQApi({ + className: NTQQApiClass.WINDOW_API, + methodName: NTQQApiMethod.OPEN_EXTRA_WINDOW, + cbCmd: ReceiveCmdS.GROUP_NOTIFY, + afterFirstCmd: false, + args: [ + "GroupNotifyFilterWindow" + ] + }) + // 关闭窗口 + setTimeout(() => { + for (const w of BrowserWindow.getAllWindows()) { + // log("close window", w.webContents.getURL()) + if (w.webContents.getURL().indexOf("#/notify-filter/") != -1) { + w.close(); + } + } + }, 2000); + return result; + } + static async handleGroupRequest(seq: string, operateType: GroupRequestOperateTypes, reason?: string) { + const notify: GroupNotify = await dbUtil.getGroupNotify(seq) + if (!notify) { + throw `${seq}对应的加群通知不存在` + } + // delete groupNotifies[seq]; + return await callNTQQApi({ + methodName: NTQQApiMethod.HANDLE_GROUP_REQUEST, + args: [ + { + "doubt": false, + "operateMsg": { + "operateType": operateType, // 2 拒绝 + "targetMsg": { + "seq": seq, // 通知序列号 + "type": notify.type, + "groupCode": notify.group.groupCode, + "postscript": reason + } + } + }, + null + ] + }); + } + static async quitGroup(groupQQ: string) { + await callNTQQApi({ + methodName: NTQQApiMethod.QUIT_GROUP, + args: [ + {"groupCode": groupQQ}, + null + ] + }) + } + static async kickMember(groupQQ: string, kickUids: string[], refuseForever: boolean = false, kickReason: string = '') { + return await callNTQQApi( + { + methodName: NTQQApiMethod.KICK_MEMBER, + args: [ + { + groupCode: groupQQ, + kickUids, + refuseForever, + kickReason, + } + ] + } + ) + } + static async banMember(groupQQ: string, memList: Array<{ uid: string, timeStamp: number }>) { + // timeStamp为秒数, 0为解除禁言 + return await callNTQQApi( + { + methodName: NTQQApiMethod.MUTE_MEMBER, + args: [ + { + groupCode: groupQQ, + memList, + } + ] + } + ) + } + static async banGroup(groupQQ: string, shutUp: boolean) { + return await callNTQQApi({ + methodName: NTQQApiMethod.MUTE_GROUP, + args: [ + { + groupCode: groupQQ, + shutUp + }, null + ] + }) + } + static async setMemberCard(groupQQ: string, memberUid: string, cardName: string) { + return await callNTQQApi({ + methodName: NTQQApiMethod.SET_MEMBER_CARD, + args: [ + { + groupCode: groupQQ, + uid: memberUid, + cardName + }, null + ] + }) + } + static async setMemberRole(groupQQ: string, memberUid: string, role: GroupMemberRole) { + return await callNTQQApi({ + methodName: NTQQApiMethod.SET_MEMBER_ROLE, + args: [ + { + groupCode: groupQQ, + uid: memberUid, + role + }, null + ] + }) + } + static async setGroupName(groupQQ: string, groupName: string) { + return await callNTQQApi({ + methodName: NTQQApiMethod.SET_GROUP_NAME, + args: [ + { + groupCode: groupQQ, + groupName + }, null + ] + }) + } + + // 头衔不可用 + static async setGroupTitle(groupQQ: string, uid: string, title: string) { + return await callNTQQApi({ + methodName: NTQQApiMethod.SET_GROUP_TITLE, + args: [ + { + groupCode: groupQQ, + uid, + title + }, null + ] + }) + } + static publishGroupBulletin(groupQQ: string, title: string, content: string) { + + } +} \ No newline at end of file diff --git a/src/ntqqapi/api/msg.ts b/src/ntqqapi/api/msg.ts new file mode 100644 index 0000000..9aa5a44 --- /dev/null +++ b/src/ntqqapi/api/msg.ts @@ -0,0 +1,163 @@ +import {callNTQQApi, GeneralCallResult, NTQQApiMethod} from "../ntcall"; +import {ChatType, RawMessage, SendMessageElement} from "../types"; +import {log, sleep} from "../../common/utils"; +import {dbUtil} from "../../common/db"; +import {selfInfo} from "../../common/data"; +import {ReceiveCmdS, registerReceiveHook} from "../hook"; +export let sendMessagePool: Record void) | null> = {}// peerUid: callbackFunnc + +export interface Peer { + chatType: ChatType + peerUid: string // 如果是群聊uid为群号,私聊uid就是加密的字符串 + guildId?: "" +} + +export class NTQQMsgApi { + static async activateChat(peer: Peer) { + return await callNTQQApi({ + methodName: NTQQApiMethod.ADD_ACTIVE_CHAT, + args: [{peer, cnt: 20}] + }) + } + static async recallMsg(peer: Peer, msgIds: string[]) { + return await callNTQQApi({ + methodName: NTQQApiMethod.RECALL_MSG, + args: [{ + peer, + msgIds + }, null] + }) + } + + static async sendMsg(peer: Peer, msgElements: SendMessageElement[], + waitComplete = true, timeout = 10000) { + const peerUid = peer.peerUid + + // 等待上一个相同的peer发送完 + let checkLastSendUsingTime = 0; + const waitLastSend = async () => { + if (checkLastSendUsingTime > timeout) { + throw ("发送超时") + } + let lastSending = sendMessagePool[peer.peerUid] + if (lastSending) { + // log("有正在发送的消息,等待中...") + await sleep(500); + checkLastSendUsingTime += 500; + return await waitLastSend(); + } else { + return; + } + } + await waitLastSend(); + + let sentMessage: RawMessage = null; + sendMessagePool[peerUid] = async (rawMessage: RawMessage) => { + delete sendMessagePool[peerUid]; + sentMessage = rawMessage; + } + + let checkSendCompleteUsingTime = 0; + const checkSendComplete = async (): Promise => { + if (sentMessage) { + if (waitComplete) { + if ((await dbUtil.getMsgByLongId(sentMessage.msgId)).sendStatus == 2) { + return sentMessage + } + } else { + return sentMessage + } + // log(`给${peerUid}发送消息成功`) + } + checkSendCompleteUsingTime += 500 + if (checkSendCompleteUsingTime > timeout) { + throw ('发送超时') + } + await sleep(500) + return await checkSendComplete() + } + + callNTQQApi({ + methodName: NTQQApiMethod.SEND_MSG, + args: [{ + msgId: "0", + peer, msgElements, + msgAttributeInfos: new Map(), + }, null] + }).then() + return await checkSendComplete() + } + + static async forwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) { + return await callNTQQApi({ + methodName: NTQQApiMethod.FORWARD_MSG, + args: [ + { + msgIds: msgIds, + srcContact: srcPeer, + dstContacts: [ + destPeer + ], + commentElements: [], + msgAttributeInfos: new Map() + }, + null, + ] + }) + } + + static async multiForwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) { + const msgInfos = msgIds.map(id => { + return {msgId: id, senderShowName: selfInfo.nick} + }) + const apiArgs = [ + { + msgInfos, + srcContact: srcPeer, + dstContact: destPeer, + commentElements: [], + msgAttributeInfos: new Map() + }, + null, + ] + return await new Promise((resolve, reject) => { + let complete = false + setTimeout(() => { + if (!complete) { + reject("转发消息超时"); + } + }, 5000) + registerReceiveHook(ReceiveCmdS.SELF_SEND_MSG, async (payload: { msgRecord: RawMessage }) => { + const msg = payload.msgRecord + // 需要判断它是转发的消息,并且识别到是当前转发的这一条 + const arkElement = msg.elements.find(ele => ele.arkElement) + if (!arkElement) { + // log("收到的不是转发消息") + return + } + const forwardData: any = JSON.parse(arkElement.arkElement.bytesData) + if (forwardData.app != 'com.tencent.multimsg') { + return + } + if (msg.peerUid == destPeer.peerUid && msg.senderUid == selfInfo.uid) { + complete = true + await dbUtil.addMsg(msg) + resolve(msg) + log('转发消息成功:', payload) + } + }) + callNTQQApi({ + methodName: NTQQApiMethod.MULTI_FORWARD_MSG, + args: apiArgs + }).then(result => { + log("转发消息结果:", result, apiArgs) + if (result.result !== 0) { + complete = true; + reject("转发消息失败," + JSON.stringify(result)); + } + }) + }) + } + + +} \ No newline at end of file diff --git a/src/ntqqapi/api/user.ts b/src/ntqqapi/api/user.ts new file mode 100644 index 0000000..131d01a --- /dev/null +++ b/src/ntqqapi/api/user.ts @@ -0,0 +1,57 @@ +import {callNTQQApi, GeneralCallResult, NTQQApiClass, NTQQApiMethod} from "../ntcall"; +import {SelfInfo, User} from "../types"; +import {ReceiveCmdS} from "../hook"; +import {uidMaps} from "../../common/data"; + + +export class NTQQUserApi{ + static async setQQAvatar(filePath: string) { + return await callNTQQApi({ + methodName: NTQQApiMethod.SET_QQ_AVATAR, + args: [{ + path:filePath + }, null], + timeoutSecond: 10 // 10秒不一定够 + }); + } + + static async getSelfInfo() { + return await callNTQQApi({ + className: NTQQApiClass.GLOBAL_DATA, + methodName: NTQQApiMethod.SELF_INFO, timeoutSecond: 2 + }) + } + static async getUserInfo(uid: string) { + const result = await callNTQQApi<{ profiles: Map }>({ + methodName: NTQQApiMethod.USER_INFO, + args: [{force: true, uids: [uid]}, undefined], + cbCmd: ReceiveCmdS.USER_INFO + }) + return result.profiles.get(uid) + } + static async getUserDetailInfo(uid: string) { + const result = await callNTQQApi<{ info: User }>({ + methodName: NTQQApiMethod.USER_DETAIL_INFO, + cbCmd: ReceiveCmdS.USER_DETAIL_INFO, + afterFirstCmd: false, + cmdCB: (payload) => { + const success = payload.info.uid == uid + // log("get user detail info", success, uid, payload) + return success + }, + args: [ + { + uid + }, + null + ] + }) + const info = result.info + if (info?.uin) { + uidMaps[info.uid] = info.uin + } + return info + } + + +} \ No newline at end of file diff --git a/src/ntqqapi/constructor.ts b/src/ntqqapi/constructor.ts index 9efedf8..342c437 100644 --- a/src/ntqqapi/constructor.ts +++ b/src/ntqqapi/constructor.ts @@ -11,10 +11,10 @@ import { SendTextElement, SendVideoElement } from "./types"; -import {NTQQApi} from "./ntcall"; import {calculateFileMD5, encodeSilk, getVideoInfo, isGIF, log, sleep} from "../common/utils"; import {promises as fs} from "node:fs"; import ffmpeg from "fluent-ffmpeg" +import {NTQQFileApi} from "./api/file"; export class SendMsgElementConstructor { @@ -60,11 +60,11 @@ export class SendMsgElementConstructor { } static async pic(picPath: string): Promise { - const {md5, fileName, path, fileSize} = await NTQQApi.uploadFile(picPath, ElementType.PIC); + const {md5, fileName, path, fileSize} = await NTQQFileApi.uploadFile(picPath, ElementType.PIC); if (fileSize === 0) { throw "文件异常,大小为0"; } - const imageSize = await NTQQApi.getImageSize(picPath); + const imageSize = await NTQQFileApi.getImageSize(picPath); const picElement = { md5HexStr: md5, fileSize: fileSize, @@ -89,7 +89,7 @@ export class SendMsgElementConstructor { } static async file(filePath: string, fileName: string = ""): Promise { - const {md5, fileName: _fileName, path, fileSize} = await NTQQApi.uploadFile(filePath, ElementType.FILE); + const {md5, fileName: _fileName, path, fileSize} = await NTQQFileApi.uploadFile(filePath, ElementType.FILE); if (fileSize === 0) { throw "文件异常,大小为0"; } @@ -107,7 +107,7 @@ export class SendMsgElementConstructor { } static async video(filePath: string, fileName: string = ""): Promise { - let {fileName: _fileName, path, fileSize, md5} = await NTQQApi.uploadFile(filePath, ElementType.VIDEO); + let {fileName: _fileName, path, fileSize, md5} = await NTQQFileApi.uploadFile(filePath, ElementType.VIDEO); if (fileSize === 0) { throw "文件异常,大小为0"; } @@ -177,7 +177,7 @@ export class SendMsgElementConstructor { static async ptt(pttPath: string): Promise { const {converted, path: silkPath, duration} = await encodeSilk(pttPath); // log("生成语音", silkPath, duration); - const {md5, fileName, path, fileSize} = await NTQQApi.uploadFile(silkPath, ElementType.PTT); + const {md5, fileName, path, fileSize} = await NTQQFileApi.uploadFile(silkPath, ElementType.PTT); if (fileSize === 0) { throw "文件异常,大小为0"; } diff --git a/src/ntqqapi/hook.ts b/src/ntqqapi/hook.ts index 3ab62b1..0e4099e 100644 --- a/src/ntqqapi/hook.ts +++ b/src/ntqqapi/hook.ts @@ -1,36 +1,41 @@ import {BrowserWindow} from 'electron'; import {getConfigUtil, log, sleep} from "../common/utils"; -import {NTQQApi, NTQQApiClass, sendMessagePool} from "./ntcall"; +import {NTQQApiClass} from "./ntcall"; +import {sendMessagePool} from "./api/msg" import {Group, RawMessage, User} from "./types"; import {friends, groups, selfInfo, tempGroupCodeMap} from "../common/data"; import {OB11GroupDecreaseEvent} from "../onebot11/event/notice/OB11GroupDecreaseEvent"; -import {OB11GroupIncreaseEvent} from "../onebot11/event/notice/OB11GroupIncreaseEvent"; import {v4 as uuidv4} from "uuid" import {postOB11Event} from "../onebot11/server/postOB11Event"; import {HOOK_LOG} from "../common/config"; import fs from "fs"; import {dbUtil} from "../common/db"; +import {NTQQGroupApi} from "./api/group"; export let hookApiCallbacks: Record void> = {} -export enum ReceiveCmd { - UPDATE_MSG = "nodeIKernelMsgListener/onMsgInfoListUpdate", - NEW_MSG = "nodeIKernelMsgListener/onRecvMsg", - SELF_SEND_MSG = "nodeIKernelMsgListener/onAddSendMsg", - USER_INFO = "nodeIKernelProfileListener/onProfileSimpleChanged", - USER_DETAIL_INFO = "nodeIKernelProfileListener/onProfileDetailInfoChanged", - GROUPS = "nodeIKernelGroupListener/onGroupListUpdate", - GROUPS_UNIX = "onGroupListUpdate", - FRIENDS = "onBuddyListChange", - MEDIA_DOWNLOAD_COMPLETE = "nodeIKernelMsgListener/onRichMediaDownloadComplete", - UNREAD_GROUP_NOTIFY = "nodeIKernelGroupListener/onGroupNotifiesUnreadCountUpdated", - GROUP_NOTIFY = "nodeIKernelGroupListener/onGroupSingleScreenNotifies", - FRIEND_REQUEST = "nodeIKernelBuddyListener/onBuddyReqChange", - SELF_STATUS = 'nodeIKernelProfileListener/onSelfStatusChanged', - CACHE_SCAN_FINISH = "nodeIKernelStorageCleanListener/onFinishScan", - MEDIA_UPLOAD_COMPLETE = "nodeIKernelMsgListener/onRichMediaUploadComplete", +export let ReceiveCmdS = { + UPDATE_MSG: "nodeIKernelMsgListener/onMsgInfoListUpdate", + UPDATE_ACTIVE_MSG: "nodeIKernelMsgListener/onActiveMsgInfoUpdate", + NEW_MSG: `nodeIKernelMsgListener/onRecvMsg`, + NEW_ACTIVE_MSG: `nodeIKernelMsgListener/onRecvActiveMsg`, + SELF_SEND_MSG: "nodeIKernelMsgListener/onAddSendMsg", + USER_INFO: "nodeIKernelProfileListener/onProfileSimpleChanged", + USER_DETAIL_INFO: "nodeIKernelProfileListener/onProfileDetailInfoChanged", + GROUPS: "nodeIKernelGroupListener/onGroupListUpdate", + GROUPS_UNIX: "onGroupListUpdate", + FRIENDS: "onBuddyListChange", + MEDIA_DOWNLOAD_COMPLETE: "nodeIKernelMsgListener/onRichMediaDownloadComplete", + UNREAD_GROUP_NOTIFY: "nodeIKernelGroupListener/onGroupNotifiesUnreadCountUpdated", + GROUP_NOTIFY: "nodeIKernelGroupListener/onGroupSingleScreenNotifies", + FRIEND_REQUEST: "nodeIKernelBuddyListener/onBuddyReqChange", + SELF_STATUS: 'nodeIKernelProfileListener/onSelfStatusChanged', + CACHE_SCAN_FINISH: "nodeIKernelStorageCleanListener/onFinishScan", + MEDIA_UPLOAD_COMPLETE: "nodeIKernelMsgListener/onRichMediaUploadComplete", } +export type ReceiveCmd = typeof ReceiveCmdS[keyof typeof ReceiveCmdS] + interface NTQQApiReturnData extends Array { 0: { "type": "request", @@ -46,7 +51,7 @@ interface NTQQApiReturnData extends Array { } let receiveHooks: Array<{ - method: ReceiveCmd, + method: ReceiveCmd[], hookFunc: ((payload: any) => void | Promise) id: string }> = [] @@ -66,7 +71,7 @@ export function hookNTQQApiReceive(window: BrowserWindow) { const ntQQApiMethodName = receiveData.cmdName; // log(`received ntqq api message: ${channel} ${ntQQApiMethodName}`, JSON.stringify(receiveData)) for (let hook of receiveHooks) { - if (hook.method === ntQQApiMethodName) { + if (hook.method.includes(ntQQApiMethodName)) { new Promise((resolve, reject) => { try { let _ = hook.hookFunc(receiveData.payload) @@ -121,8 +126,11 @@ export function hookNTQQApiCall(window: BrowserWindow) { } } -export function registerReceiveHook(method: ReceiveCmd, hookFunc: (payload: PayloadType) => void): string { +export function registerReceiveHook(method: ReceiveCmd | ReceiveCmd[], hookFunc: (payload: PayloadType) => void): string { const id = uuidv4() + if (!Array.isArray(method)) { + method = [method] + } receiveHooks.push({ method, hookFunc, @@ -147,7 +155,7 @@ async function updateGroups(_groups: Group[], needUpdate: boolean = true) { } if (needUpdate) { - const members = await NTQQApi.getGroupMembers(group.groupCode); + const members = await NTQQGroupApi.getGroupMembers(group.groupCode); if (members) { existGroup.members = members; @@ -166,7 +174,7 @@ async function processGroupEvent(payload) { const oldMembers = existGroup.members; await sleep(200); // 如果请求QQ API的速度过快,通常无法正确拉取到最新的群信息,因此这里人为引入一个延时 - const newMembers = await NTQQApi.getGroupMembers(group.groupCode); + const newMembers = await NTQQGroupApi.getGroupMembers(group.groupCode); group.members = newMembers; const newMembersSet = new Set(); // 建立索引降低时间复杂度 @@ -193,7 +201,7 @@ async function processGroupEvent(payload) { } // 群列表变动 -registerReceiveHook<{ groupList: Group[], updateType: number }>(ReceiveCmd.GROUPS, (payload) => { +registerReceiveHook<{ groupList: Group[], updateType: number }>(ReceiveCmdS.GROUPS, (payload) => { if (payload.updateType != 2) { updateGroups(payload.groupList).then(); } else { @@ -202,7 +210,7 @@ registerReceiveHook<{ groupList: Group[], updateType: number }>(ReceiveCmd.GROUP } } }) -registerReceiveHook<{ groupList: Group[], updateType: number }>(ReceiveCmd.GROUPS_UNIX, (payload) => { +registerReceiveHook<{ groupList: Group[], updateType: number }>(ReceiveCmdS.GROUPS_UNIX, (payload) => { if (payload.updateType != 2) { updateGroups(payload.groupList).then(); } else { @@ -215,7 +223,7 @@ registerReceiveHook<{ groupList: Group[], updateType: number }>(ReceiveCmd.GROUP // 好友列表变动 registerReceiveHook<{ data: { categoryId: number, categroyName: string, categroyMbCount: number, buddyList: User[] }[] -}>(ReceiveCmd.FRIENDS, payload => { +}>(ReceiveCmdS.FRIENDS, payload => { for (const fData of payload.data) { const _friends = fData.buddyList; for (let friend of _friends) { @@ -230,7 +238,7 @@ registerReceiveHook<{ }) // 新消息 -registerReceiveHook<{ msgList: Array }>(ReceiveCmd.NEW_MSG, (payload) => { +registerReceiveHook<{ msgList: Array }>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], (payload) => { const {autoDeleteFile} = getConfigUtil().getConfig(); if (!autoDeleteFile) { return @@ -270,7 +278,7 @@ registerReceiveHook<{ msgList: Array }>(ReceiveCmd.NEW_MSG, (payload } }) -registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmd.SELF_SEND_MSG, ({msgRecord}) => { +registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, ({msgRecord}) => { const message = msgRecord; const peerUid = message.peerUid; // log("收到自己发送成功的消息", Object.keys(sendMessagePool), message); @@ -286,6 +294,6 @@ registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmd.SELF_SEND_MSG, ({msgRe } }) -registerReceiveHook<{ info: { status: number } }>(ReceiveCmd.SELF_STATUS, (info) => { +registerReceiveHook<{ info: { status: number } }>(ReceiveCmdS.SELF_STATUS, (info) => { selfInfo.online = info.info.status !== 20 }) diff --git a/src/ntqqapi/ntcall.ts b/src/ntqqapi/ntcall.ts index e14801d..9ffd3f7 100644 --- a/src/ntqqapi/ntcall.ts +++ b/src/ntqqapi/ntcall.ts @@ -1,42 +1,8 @@ -import {BrowserWindow, ipcMain} from "electron"; +import {ipcMain} from "electron"; import {hookApiCallbacks, ReceiveCmd, registerReceiveHook, removeReceiveHook} from "./hook"; -import {log, sleep} from "../common/utils"; -import { - ChatType, - ElementType, - Friend, - FriendRequest, - Group, - GroupMember, - GroupMemberRole, - GroupNotifies, - GroupNotify, - GroupRequestOperateTypes, - RawMessage, - SelfInfo, - SendMessageElement, - User, - CacheScanResult, - ChatCacheList, ChatCacheListItemBasic, - CacheFileList, CacheFileListItem, CacheFileType, -} from "./types"; -import * as fs from "fs"; -import {friendRequests, selfInfo, uidMaps} from "../common/data"; +import {log} from "../common/utils"; + import {v4 as uuidv4} from "uuid" -import path from "path"; -import {dbUtil} from "../common/db"; - -interface IPCReceiveEvent { - eventName: string - callbackId: string -} - -export type IPCReceiveDetail = [ - { - cmdName: NTQQApiMethod - payload: unknown - }, -] export enum NTQQApiClass { NT_API = "ns-ntApi", @@ -49,7 +15,7 @@ export enum NTQQApiClass { } export enum NTQQApiMethod { - SET_HEADER = "nodeIKernelProfileService/setHeader", + ADD_ACTIVE_CHAT = "nodeIKernelMsgService/getAioFirstViewLatestMsgsAndAddActiveChat", // 激活群助手内的聊天窗口,这样才能收到消息 LIKE_FRIEND = "nodeIKernelProfileLikeService/setBuddyProfileLike", SELF_INFO = "fetchAuthData", FRIENDS = "nodeIKernelBuddyService/getBuddyList", @@ -106,12 +72,6 @@ enum NTQQApiChannel { IPC_UP_1 = "IPC_UP_1", } -export interface Peer { - chatType: ChatType - peerUid: string // 如果是群聊uid为群号,私聊uid就是加密的字符串 - guildId?: "" -} - interface NTQQApiParams { methodName: NTQQApiMethod | string, className?: NTQQApiClass, @@ -124,7 +84,7 @@ interface NTQQApiParams { timeoutSecond?: number, } -function callNTQQApi(params: NTQQApiParams) { +export function callNTQQApi(params: NTQQApiParams) { let { className, methodName, channel, args, cbCmd, timeoutSecond: timeout, @@ -199,574 +159,13 @@ function callNTQQApi(params: NTQQApiParams) { } -export let sendMessagePool: Record void) | null> = {}// peerUid: callbackFunnc - -interface GeneralCallResult { +export interface GeneralCallResult { result: number, // 0: success errMsg: string } export class NTQQApi { - static async setHeader(path: string) { - return await callNTQQApi({ - methodName: NTQQApiMethod.SET_HEADER, - args: [path] - }) - } - - static async likeFriend(uid: string, count = 1) { - return await callNTQQApi({ - methodName: NTQQApiMethod.LIKE_FRIEND, - args: [{ - doLikeUserInfo: { - friendUid: uid, - sourceId: 71, - doLikeCount: count, - doLikeTollCount: 0 - } - }, null] - }) - } - - static async getSelfInfo() { - return await callNTQQApi({ - className: NTQQApiClass.GLOBAL_DATA, - methodName: NTQQApiMethod.SELF_INFO, timeoutSecond: 2 - }) - } - - static async getUserInfo(uid: string) { - const result = await callNTQQApi<{ profiles: Map }>({ - methodName: NTQQApiMethod.USER_INFO, - args: [{force: true, uids: [uid]}, undefined], - cbCmd: ReceiveCmd.USER_INFO - }) - return result.profiles.get(uid) - } - - static async getUserDetailInfo(uid: string) { - const result = await callNTQQApi<{ info: User }>({ - methodName: NTQQApiMethod.USER_DETAIL_INFO, - cbCmd: ReceiveCmd.USER_DETAIL_INFO, - afterFirstCmd: false, - cmdCB: (payload) => { - const success = payload.info.uid == uid - // log("get user detail info", success, uid, payload) - return success - }, - args: [ - { - uid - }, - null - ] - }) - const info = result.info - if (info?.uin) { - uidMaps[info.uid] = info.uin - } - return info - } - - static async getFriends(forced = false) { - const data = await callNTQQApi<{ - data: { - categoryId: number, - categroyName: string, - categroyMbCount: number, - buddyList: Friend[] - }[] - }>( - { - methodName: NTQQApiMethod.FRIENDS, - args: [{force_update: forced}, undefined], - cbCmd: ReceiveCmd.FRIENDS - }) - let _friends: Friend[] = []; - for (const fData of data.data) { - _friends.push(...fData.buddyList) - } - return _friends - } - - static async getGroups(forced = false) { - let cbCmd = ReceiveCmd.GROUPS - if (process.platform != "win32") { - cbCmd = ReceiveCmd.GROUPS_UNIX - } - const result = await callNTQQApi<{ - updateType: number, - groupList: Group[] - }>({methodName: NTQQApiMethod.GROUPS, args: [{force_update: forced}, undefined], cbCmd}) - return result.groupList - } - - static async getGroupMembers(groupQQ: string, num = 3000): Promise { - const sceneId = await callNTQQApi({ - methodName: NTQQApiMethod.GROUP_MEMBER_SCENE, - args: [{ - groupCode: groupQQ, - scene: "groupMemberList_MainWindow" - }] - }) - // log("get group member sceneId", sceneId); - try { - const result = await callNTQQApi<{ - result: { infos: any } - }>({ - methodName: NTQQApiMethod.GROUP_MEMBERS, - args: [{ - sceneId: sceneId, - num: num - }, - null - ] - }) - // log("members info", typeof result.result.infos, Object.keys(result.result.infos)) - const values = result.result.infos.values() - - const members: GroupMember[] = Array.from(values) - for (const member of members) { - uidMaps[member.uid] = member.uin; - } - // log(uidMaps); - // log("members info", values); - log(`get group ${groupQQ} members success`) - return members - } catch (e) { - log(`get group ${groupQQ} members failed`, e) - return [] - } - } - - static async getFileType(filePath: string) { - return await callNTQQApi<{ ext: string }>({ - className: NTQQApiClass.FS_API, methodName: NTQQApiMethod.FILE_TYPE, args: [filePath] - }) - } - - static async getFileMd5(filePath: string) { - return await callNTQQApi({ - className: NTQQApiClass.FS_API, - methodName: NTQQApiMethod.FILE_MD5, - args: [filePath] - }) - } - - static async copyFile(filePath: string, destPath: string) { - return await callNTQQApi({ - className: NTQQApiClass.FS_API, - methodName: NTQQApiMethod.FILE_COPY, - args: [{ - fromPath: filePath, - toPath: destPath - }] - }) - } - - static async getImageSize(filePath: string) { - return await callNTQQApi<{ width: number, height: number }>({ - className: NTQQApiClass.FS_API, methodName: NTQQApiMethod.IMAGE_SIZE, args: [filePath] - }) - } - - static async getFileSize(filePath: string) { - return await callNTQQApi({ - className: NTQQApiClass.FS_API, methodName: NTQQApiMethod.FILE_SIZE, args: [filePath] - }) - } - - // 上传文件到QQ的文件夹 - static async uploadFile(filePath: string, elementType: ElementType = ElementType.PIC) { - const md5 = await NTQQApi.getFileMd5(filePath); - let ext = (await NTQQApi.getFileType(filePath))?.ext - if (ext) { - ext = "." + ext - } else { - ext = "" - } - let fileName = `${path.basename(filePath)}`; - if (fileName.indexOf(".") === -1) { - fileName += ext; - } - const mediaPath = await callNTQQApi({ - methodName: NTQQApiMethod.MEDIA_FILE_PATH, - args: [{ - path_info: { - md5HexStr: md5, - fileName: fileName, - elementType: elementType, - elementSubType: 0, - thumbSize: 0, - needCreate: true, - downloadType: 1, - file_uuid: "" - } - }] - }) - log("media path", mediaPath) - await NTQQApi.copyFile(filePath, mediaPath); - const fileSize = await NTQQApi.getFileSize(filePath); - return { - md5, - fileName, - path: mediaPath, - fileSize - } - } - - static async downloadMedia(msgId: string, chatType: ChatType, peerUid: string, elementId: string, thumbPath: string, sourcePath: string) { - // 用于下载收到的消息中的图片等 - if (fs.existsSync(sourcePath)) { - return sourcePath - } - const apiParams = [ - { - getReq: { - msgId: msgId, - chatType: chatType, - peerUid: peerUid, - elementId: elementId, - thumbSize: 0, - downloadType: 1, - filePath: thumbPath, - }, - }, - undefined, - ] - // log("需要下载media", sourcePath); - await callNTQQApi({ - methodName: NTQQApiMethod.DOWNLOAD_MEDIA, - args: apiParams, - cbCmd: ReceiveCmd.MEDIA_DOWNLOAD_COMPLETE, - cmdCB: (payload: { notifyInfo: { filePath: string } }) => { - // log("media 下载完成判断", payload.notifyInfo.filePath, sourcePath); - return payload.notifyInfo.filePath == sourcePath; - } - }) - return sourcePath - } - - static async recallMsg(peer: Peer, msgIds: string[]) { - return await callNTQQApi({ - methodName: NTQQApiMethod.RECALL_MSG, - args: [{ - peer, - msgIds - }, null] - }) - } - - static async sendMsg(peer: Peer, msgElements: SendMessageElement[], waitComplete = true, timeout = 10000) { - const peerUid = peer.peerUid - - // 等待上一个相同的peer发送完 - let checkLastSendUsingTime = 0; - const waitLastSend = async () => { - if (checkLastSendUsingTime > timeout) { - throw ("发送超时") - } - let lastSending = sendMessagePool[peer.peerUid] - if (lastSending) { - // log("有正在发送的消息,等待中...") - await sleep(500); - checkLastSendUsingTime += 500; - return await waitLastSend(); - } else { - return; - } - } - await waitLastSend(); - - let sentMessage: RawMessage = null; - sendMessagePool[peerUid] = async (rawMessage: RawMessage) => { - delete sendMessagePool[peerUid]; - sentMessage = rawMessage; - } - - let checkSendCompleteUsingTime = 0; - const checkSendComplete = async (): Promise => { - if (sentMessage) { - if (waitComplete) { - if ((await dbUtil.getMsgByLongId(sentMessage.msgId)).sendStatus == 2) { - return sentMessage - } - } else { - return sentMessage - } - // log(`给${peerUid}发送消息成功`) - } - checkSendCompleteUsingTime += 500 - if (checkSendCompleteUsingTime > timeout) { - throw ('发送超时') - } - await sleep(500) - return await checkSendComplete() - } - - callNTQQApi({ - methodName: NTQQApiMethod.SEND_MSG, - args: [{ - msgId: "0", - peer, msgElements, - msgAttributeInfos: new Map(), - }, null] - }).then() - return await checkSendComplete() - } - - static async forwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) { - return await callNTQQApi({ - methodName: NTQQApiMethod.FORWARD_MSG, - args: [ - { - msgIds: msgIds, - srcContact: srcPeer, - dstContacts: [ - destPeer - ], - commentElements: [], - msgAttributeInfos: new Map() - }, - null, - ] - }) - - } - - static async multiForwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) { - const msgInfos = msgIds.map(id => { - return {msgId: id, senderShowName: selfInfo.nick} - }) - const apiArgs = [ - { - msgInfos, - srcContact: srcPeer, - dstContact: destPeer, - commentElements: [], - msgAttributeInfos: new Map() - }, - null, - ] - return await new Promise((resolve, reject) => { - let complete = false - setTimeout(() => { - if (!complete) { - reject("转发消息超时"); - } - }, 5000) - registerReceiveHook(ReceiveCmd.SELF_SEND_MSG, async (payload: { msgRecord: RawMessage }) => { - const msg = payload.msgRecord - // 需要判断它是转发的消息,并且识别到是当前转发的这一条 - const arkElement = msg.elements.find(ele => ele.arkElement) - if (!arkElement) { - // log("收到的不是转发消息") - return - } - const forwardData: any = JSON.parse(arkElement.arkElement.bytesData) - if (forwardData.app != 'com.tencent.multimsg') { - return - } - if (msg.peerUid == destPeer.peerUid && msg.senderUid == selfInfo.uid) { - complete = true - await dbUtil.addMsg(msg) - resolve(msg) - log('转发消息成功:', payload) - } - }) - callNTQQApi({ - methodName: NTQQApiMethod.MULTI_FORWARD_MSG, - args: apiArgs - }).then(result => { - log("转发消息结果:", result, apiArgs) - if (result.result !== 0) { - complete = true; - reject("转发消息失败," + JSON.stringify(result)); - } - }) - }) - } - - static async getGroupNotifies() { - // 获取管理员变更 - // 加群通知,退出通知,需要管理员权限 - callNTQQApi({ - methodName: ReceiveCmd.GROUP_NOTIFY, - classNameIsRegister: true, - }).then() - return await callNTQQApi({ - methodName: NTQQApiMethod.GET_GROUP_NOTICE, - cbCmd: ReceiveCmd.GROUP_NOTIFY, - afterFirstCmd: false, - args: [ - {"doubt": false, "startSeq": "", "number": 14}, - null - ] - }); - } - - static async getGroupIgnoreNotifies() { - await NTQQApi.getGroupNotifies(); - const result = callNTQQApi({ - className: NTQQApiClass.WINDOW_API, - methodName: NTQQApiMethod.OPEN_EXTRA_WINDOW, - cbCmd: ReceiveCmd.GROUP_NOTIFY, - afterFirstCmd: false, - args: [ - "GroupNotifyFilterWindow" - ] - }) - // 关闭窗口 - setTimeout(() => { - for (const w of BrowserWindow.getAllWindows()) { - // log("close window", w.webContents.getURL()) - if (w.webContents.getURL().indexOf("#/notify-filter/") != -1) { - w.close(); - } - } - }, 2000); - return result; - } - - static async handleGroupRequest(seq: string, operateType: GroupRequestOperateTypes, reason?: string) { - const notify: GroupNotify = await dbUtil.getGroupNotify(seq) - if (!notify) { - throw `${seq}对应的加群通知不存在` - } - // delete groupNotifies[seq]; - return await callNTQQApi({ - methodName: NTQQApiMethod.HANDLE_GROUP_REQUEST, - args: [ - { - "doubt": false, - "operateMsg": { - "operateType": operateType, // 2 拒绝 - "targetMsg": { - "seq": seq, // 通知序列号 - "type": notify.type, - "groupCode": notify.group.groupCode, - "postscript": reason - } - } - }, - null - ] - }); - } - - static async quitGroup(groupQQ: string) { - await callNTQQApi({ - methodName: NTQQApiMethod.QUIT_GROUP, - args: [ - {"groupCode": groupQQ}, - null - ] - }) - } - - static async handleFriendRequest(sourceId: number, accept: boolean,) { - const request: FriendRequest = friendRequests[sourceId] - if (!request) { - throw `sourceId ${sourceId}, 对应的好友请求不存在` - } - const result = await callNTQQApi({ - methodName: NTQQApiMethod.HANDLE_FRIEND_REQUEST, - args: [ - { - "approvalInfo": { - "friendUid": request.friendUid, - "reqTime": request.reqTime, - accept - } - } - ] - }) - delete friendRequests[sourceId]; - return result; - } - - static async kickMember(groupQQ: string, kickUids: string[], refuseForever: boolean = false, kickReason: string = '') { - return await callNTQQApi( - { - methodName: NTQQApiMethod.KICK_MEMBER, - args: [ - { - groupCode: groupQQ, - kickUids, - refuseForever, - kickReason, - } - ] - } - ) - } - - static async banMember(groupQQ: string, memList: Array<{ uid: string, timeStamp: number }>) { - // timeStamp为秒数, 0为解除禁言 - return await callNTQQApi( - { - methodName: NTQQApiMethod.MUTE_MEMBER, - args: [ - { - groupCode: groupQQ, - memList, - } - ] - } - ) - } - - static async banGroup(groupQQ: string, shutUp: boolean) { - return await callNTQQApi({ - methodName: NTQQApiMethod.MUTE_GROUP, - args: [ - { - groupCode: groupQQ, - shutUp - }, null - ] - }) - } - - static async setMemberCard(groupQQ: string, memberUid: string, cardName: string) { - return await callNTQQApi({ - methodName: NTQQApiMethod.SET_MEMBER_CARD, - args: [ - { - groupCode: groupQQ, - uid: memberUid, - cardName - }, null - ] - }) - } - - static async setMemberRole(groupQQ: string, memberUid: string, role: GroupMemberRole) { - return await callNTQQApi({ - methodName: NTQQApiMethod.SET_MEMBER_ROLE, - args: [ - { - groupCode: groupQQ, - uid: memberUid, - role - }, null - ] - }) - } - - static async setGroupName(groupQQ: string, groupName: string) { - return await callNTQQApi({ - methodName: NTQQApiMethod.SET_GROUP_NAME, - args: [ - { - groupCode: groupQQ, - groupName - }, null - ] - }) - } - static async call(className: NTQQApiClass, cmdName: string, args: any[],) { return await callNTQQApi({ className, @@ -777,133 +176,4 @@ export class NTQQApi { }) } - static async setGroupTitle(groupQQ: string, uid: string, title: string) { - return await callNTQQApi({ - methodName: NTQQApiMethod.SET_GROUP_TITLE, - args: [ - { - groupCode: groupQQ, - uid, - title - }, null - ] - }) - } - - static publishGroupBulletin(groupQQ: string, title: string, content: string) { - - } - - static async setCacheSilentScan(isSilent: boolean = true) { - return await callNTQQApi({ - methodName: NTQQApiMethod.CACHE_SET_SILENCE, - args: [{ - isSilent - }, null] - }); - } - - static addCacheScannedPaths(pathMap: object = {}) { - return callNTQQApi({ - methodName: NTQQApiMethod.CACHE_ADD_SCANNED_PATH, - args: [{ - pathMap: {...pathMap}, - }, null] - }); - } - - static scanCache() { - callNTQQApi({ - methodName: ReceiveCmd.CACHE_SCAN_FINISH, - classNameIsRegister: true, - }).then(); - return callNTQQApi({ - methodName: NTQQApiMethod.CACHE_SCAN, - args: [null, null], - timeoutSecond: 300, - }); - } - - static getHotUpdateCachePath() { - return callNTQQApi({ - className: NTQQApiClass.HOTUPDATE_API, - methodName: NTQQApiMethod.CACHE_PATH_HOT_UPDATE - }); - } - - static getDesktopTmpPath() { - return callNTQQApi({ - className: NTQQApiClass.BUSINESS_API, - methodName: NTQQApiMethod.CACHE_PATH_DESKTOP_TEMP - }); - } - - static getCacheSessionPathList() { - return callNTQQApi<{ - key: string, - value: string - }[]>({ - className: NTQQApiClass.OS_API, - methodName: NTQQApiMethod.CACHE_PATH_SESSION, - }); - } - - static clearCache(cacheKeys: Array = ['tmp', 'hotUpdate']) { - return callNTQQApi({ // TODO: 目前还不知道真正的返回值是什么 - methodName: NTQQApiMethod.CACHE_CLEAR, - args: [{ - keys: cacheKeys - }, null] - }); - } - - static getChatCacheList(type: ChatType, pageSize: number = 1000, pageIndex: number = 0) { - return new Promise((res, rej) => { - callNTQQApi({ - methodName: NTQQApiMethod.CACHE_CHAT_GET, - args: [{ - chatType: type, - pageSize, - order: 1, - pageIndex - }, null] - }).then(list => res(list)) - .catch(e => rej(e)); - }); - } - - static getFileCacheInfo(fileType: CacheFileType, pageSize: number = 1000, lastRecord?: CacheFileListItem) { - const _lastRecord = lastRecord ? lastRecord : {fileType: fileType}; - - return callNTQQApi({ - methodName: NTQQApiMethod.CACHE_FILE_GET, - args: [{ - fileType: fileType, - restart: true, - pageSize: pageSize, - order: 1, - lastRecord: _lastRecord, - }, null] - }) - } - - static async clearChatCache(chats: ChatCacheListItemBasic[] = [], fileKeys: string[] = []) { - return await callNTQQApi({ - methodName: NTQQApiMethod.CACHE_CHAT_CLEAR, - args: [{ - chats, - fileKeys - }, null] - }); - } - - static async setQQAvatar(filePath: string) { - return await callNTQQApi({ - methodName: NTQQApiMethod.SET_QQ_AVATAR, - args: [{ - path:filePath - }, null], - timeoutSecond: 10 // 10秒不一定够 - }); - } } \ No newline at end of file diff --git a/src/onebot11/action/CleanCache.ts b/src/onebot11/action/CleanCache.ts index f084bbd..0ed48ff 100644 --- a/src/onebot11/action/CleanCache.ts +++ b/src/onebot11/action/CleanCache.ts @@ -1,6 +1,5 @@ import BaseAction from "./BaseAction"; import {ActionName} from "./types"; -import {NTQQApi} from "../../ntqqapi/ntcall"; import fs from "fs"; import Path from "path"; import { @@ -9,6 +8,7 @@ import { CacheFileType } from '../../ntqqapi/types'; import {dbUtil} from "../../common/db"; +import {NTQQFileApi, NTQQFileCacheApi} from "../../ntqqapi/api/file"; export default class CleanCache extends BaseAction { actionName = ActionName.CleanCache @@ -19,21 +19,21 @@ export default class CleanCache extends BaseAction { // dbUtil.clearCache(); const cacheFilePaths: string[] = []; - await NTQQApi.setCacheSilentScan(false); + await NTQQFileCacheApi.setCacheSilentScan(false); - cacheFilePaths.push((await NTQQApi.getHotUpdateCachePath())); - cacheFilePaths.push((await NTQQApi.getDesktopTmpPath())); - (await NTQQApi.getCacheSessionPathList()).forEach(e => cacheFilePaths.push(e.value)); + cacheFilePaths.push((await NTQQFileCacheApi.getHotUpdateCachePath())); + cacheFilePaths.push((await NTQQFileCacheApi.getDesktopTmpPath())); + (await NTQQFileCacheApi.getCacheSessionPathList()).forEach(e => cacheFilePaths.push(e.value)); // await NTQQApi.addCacheScannedPaths(); // XXX: 调用就崩溃,原因目前还未知 - const cacheScanResult = await NTQQApi.scanCache(); + const cacheScanResult = await NTQQFileCacheApi.scanCache(); const cacheSize = parseInt(cacheScanResult.size[6]); if (cacheScanResult.result !== 0) { throw('Something went wrong while scanning cache. Code: ' + cacheScanResult.result); } - await NTQQApi.setCacheSilentScan(true); + await NTQQFileCacheApi.setCacheSilentScan(true); if (cacheSize > 0 && cacheFilePaths.length > 2) { // 存在缓存文件且大小不为 0 时执行清理动作 // await NTQQApi.clearCache([ 'tmp', 'hotUpdate', ...cacheScanResult ]) // XXX: 也是调用就崩溃,调用 fs 删除得了 deleteCachePath(cacheFilePaths); @@ -55,11 +55,11 @@ export default class CleanCache extends BaseAction { const fileTypeAny: any = CacheFileType[name]; const fileType: CacheFileType = fileTypeAny; - cacheFileList.push(...(await NTQQApi.getFileCacheInfo(fileType)).infos.map(file => file.fileKey)); + cacheFileList.push(...(await NTQQFileCacheApi.getFileCacheInfo(fileType)).infos.map(file => file.fileKey)); } // 一并清除 - await NTQQApi.clearChatCache(chatCacheList, cacheFileList); + await NTQQFileCacheApi.clearChatCache(chatCacheList, cacheFileList); res(); } catch(e) { console.error('清理缓存时发生了错误'); @@ -89,7 +89,7 @@ function deleteCachePath(pathList: string[]) { function getCacheList(type: ChatType) { // NOTE: 做这个方法主要是因为目前还不支持针对频道消息的清理 return new Promise>((res, rej) => { - NTQQApi.getChatCacheList(type, 1000, 0) + NTQQFileCacheApi.getChatCacheList(type, 1000, 0) .then(data => { const list = data.infos.filter(e => e.chatType === type && parseInt(e.basicChatCacheInfo.chatSize) > 0); const result = list.map(e => { diff --git a/src/onebot11/action/DeleteMsg.ts b/src/onebot11/action/DeleteMsg.ts index 900d350..f98de36 100644 --- a/src/onebot11/action/DeleteMsg.ts +++ b/src/onebot11/action/DeleteMsg.ts @@ -1,7 +1,7 @@ import {ActionName} from "./types"; import BaseAction from "./BaseAction"; -import {NTQQApi} from "../../ntqqapi/ntcall"; import {dbUtil} from "../../common/db"; +import {NTQQMsgApi} from "../../ntqqapi/api/msg"; interface Payload { message_id: number @@ -12,7 +12,7 @@ class DeleteMsg extends BaseAction { protected async _handle(payload: Payload) { let msg = await dbUtil.getMsgByShortId(payload.message_id) - await NTQQApi.recallMsg({ + await NTQQMsgApi.recallMsg({ chatType: msg.chatType, peerUid: msg.peerUid }, [msg.msgId]) diff --git a/src/onebot11/action/GetGroupMemberList.ts b/src/onebot11/action/GetGroupMemberList.ts index 387b4f3..0e46610 100644 --- a/src/onebot11/action/GetGroupMemberList.ts +++ b/src/onebot11/action/GetGroupMemberList.ts @@ -1,9 +1,9 @@ import {OB11GroupMember} from '../types'; import {getGroup} from "../../common/data"; -import {NTQQApi} from "../../ntqqapi/ntcall"; import {OB11Constructor} from "../constructor"; import BaseAction from "./BaseAction"; import {ActionName} from "./types"; +import {NTQQGroupApi} from "../../ntqqapi/api/group"; export interface PayloadType { group_id: number @@ -17,7 +17,7 @@ class GetGroupMemberList extends BaseAction { const group = await getGroup(payload.group_id.toString()); if (group) { if (!group.members?.length) { - group.members = await NTQQApi.getGroupMembers(payload.group_id.toString()) + group.members = await NTQQGroupApi.getGroupMembers(payload.group_id.toString()) } return OB11Constructor.groupMembers(group); } else { diff --git a/src/onebot11/action/SendLike.ts b/src/onebot11/action/SendLike.ts index fc87aa3..66e1bcb 100644 --- a/src/onebot11/action/SendLike.ts +++ b/src/onebot11/action/SendLike.ts @@ -1,8 +1,8 @@ import BaseAction from "./BaseAction"; import {getFriend, getUidByUin, uidMaps} from "../../common/data"; -import {NTQQApi} from "../../ntqqapi/ntcall"; import {ActionName} from "./types"; import {log} from "../../common/utils"; +import {NTQQFriendApi} from "../../ntqqapi/api/friend"; interface Payload { user_id: number, @@ -23,7 +23,7 @@ export default class SendLike extends BaseAction { } else { uid = friend.uid } - let result = await NTQQApi.likeFriend(uid, parseInt(payload.times?.toString()) || 1); + let result = await NTQQFriendApi.likeFriend(uid, parseInt(payload.times?.toString()) || 1); if (result.result !== 0) { throw result.errMsg } diff --git a/src/onebot11/action/SendMsg.ts b/src/onebot11/action/SendMsg.ts index 914c8c2..1784640 100644 --- a/src/onebot11/action/SendMsg.ts +++ b/src/onebot11/action/SendMsg.ts @@ -16,7 +16,7 @@ import { OB11MessageNode, OB11PostSendMsg } from '../types'; -import {NTQQApi, Peer} from "../../ntqqapi/ntcall"; +import {Peer} from "../../ntqqapi/api/msg"; import {SendMsgElementConstructor} from "../../ntqqapi/constructor"; import {uri2local} from "../utils"; import BaseAction from "./BaseAction"; @@ -26,6 +26,7 @@ import {log, sleep} from "../../common/utils"; import {decodeCQCode} from "../cqcode"; import {dbUtil} from "../../common/db"; import {ALLOW_SEND_TEMP_MSG} from "../../common/config"; +import {NTQQMsgApi} from "../../ntqqapi/api/msg"; function checkSendMessage(sendMsgList: OB11MessageData[]) { function checkUri(uri: string): boolean { @@ -208,7 +209,7 @@ export class SendMsg extends BaseAction { } log("克隆消息", sendElements) try { - const nodeMsg = await NTQQApi.sendMsg({ + const nodeMsg = await NTQQMsgApi.sendMsg({ chatType: ChatType.friend, peerUid: selfInfo.uid }, sendElements, true); @@ -330,7 +331,7 @@ export class SendMsg extends BaseAction { // 开发转发 try { log("开发转发", nodeMsgIds) - return await NTQQApi.multiForwardMsg(srcPeer, destPeer, nodeMsgIds) + return await NTQQMsgApi.multiForwardMsg(srcPeer, destPeer, nodeMsgIds) } catch (e) { log("forward failed", e) return null; @@ -449,7 +450,7 @@ export class SendMsg extends BaseAction { if (!sendElements.length) { throw ("消息体无法解析") } - const returnMsg = await NTQQApi.sendMsg(peer, sendElements, waitComplete, 20000); + const returnMsg = await NTQQMsgApi.sendMsg(peer, sendElements, waitComplete, 20000); log("消息发送结果", returnMsg) returnMsg.msgShortId = await dbUtil.addMsg(returnMsg) deleteAfterSentFiles.map(f => fs.unlink(f, () => { diff --git a/src/onebot11/action/SetFriendAddRequest.ts b/src/onebot11/action/SetFriendAddRequest.ts index ecb07b8..e58646b 100644 --- a/src/onebot11/action/SetFriendAddRequest.ts +++ b/src/onebot11/action/SetFriendAddRequest.ts @@ -1,6 +1,6 @@ import BaseAction from "./BaseAction"; -import {NTQQApi} from "../../ntqqapi/ntcall"; import {ActionName} from "./types"; +import {NTQQFriendApi} from "../../ntqqapi/api/friend"; interface Payload { flag: string, @@ -12,7 +12,7 @@ export default class SetFriendAddRequest extends BaseAction { actionName = ActionName.SetFriendAddRequest; protected async _handle(payload: Payload): Promise { - await NTQQApi.handleFriendRequest(parseInt(payload.flag), payload.approve) + await NTQQFriendApi.handleFriendRequest(parseInt(payload.flag), payload.approve) return null; } } \ No newline at end of file diff --git a/src/onebot11/action/SetGroupAddRequest.ts b/src/onebot11/action/SetGroupAddRequest.ts index 53f3cec..8057690 100644 --- a/src/onebot11/action/SetGroupAddRequest.ts +++ b/src/onebot11/action/SetGroupAddRequest.ts @@ -1,7 +1,7 @@ import BaseAction from "./BaseAction"; import {GroupRequestOperateTypes} from "../../ntqqapi/types"; -import {NTQQApi} from "../../ntqqapi/ntcall"; import {ActionName} from "./types"; +import {NTQQGroupApi} from "../../ntqqapi/api/group"; interface Payload { flag: string, @@ -16,7 +16,7 @@ export default class SetGroupAddRequest extends BaseAction { protected async _handle(payload: Payload): Promise { const seq = payload.flag.toString(); - await NTQQApi.handleGroupRequest(seq, + await NTQQGroupApi.handleGroupRequest(seq, payload.approve ? GroupRequestOperateTypes.approve : GroupRequestOperateTypes.reject, payload.reason ) diff --git a/src/onebot11/action/SetGroupAdmin.ts b/src/onebot11/action/SetGroupAdmin.ts index 6413eea..e7143aa 100644 --- a/src/onebot11/action/SetGroupAdmin.ts +++ b/src/onebot11/action/SetGroupAdmin.ts @@ -1,8 +1,8 @@ import BaseAction from "./BaseAction"; -import {NTQQApi} from "../../ntqqapi/ntcall"; import {getGroupMember} from "../../common/data"; import {GroupMemberRole} from "../../ntqqapi/types"; import {ActionName} from "./types"; +import {NTQQGroupApi} from "../../ntqqapi/api/group"; interface Payload { group_id: number, @@ -18,7 +18,7 @@ export default class SetGroupAdmin extends BaseAction { if (!member) { throw `群成员${payload.user_id}不存在` } - await NTQQApi.setMemberRole(payload.group_id.toString(), member.uid, payload.enable ? GroupMemberRole.admin : GroupMemberRole.normal) + await NTQQGroupApi.setMemberRole(payload.group_id.toString(), member.uid, payload.enable ? GroupMemberRole.admin : GroupMemberRole.normal) return null } } \ No newline at end of file diff --git a/src/onebot11/action/SetGroupBan.ts b/src/onebot11/action/SetGroupBan.ts index 6f96cdc..cbe7ea0 100644 --- a/src/onebot11/action/SetGroupBan.ts +++ b/src/onebot11/action/SetGroupBan.ts @@ -1,7 +1,7 @@ import BaseAction from "./BaseAction"; -import {NTQQApi} from "../../ntqqapi/ntcall"; import {getGroupMember} from "../../common/data"; import {ActionName} from "./types"; +import {NTQQGroupApi} from "../../ntqqapi/api/group"; interface Payload { group_id: number, @@ -17,7 +17,7 @@ export default class SetGroupBan extends BaseAction { if (!member) { throw `群成员${payload.user_id}不存在` } - await NTQQApi.banMember(payload.group_id.toString(), + await NTQQGroupApi.banMember(payload.group_id.toString(), [{uid: member.uid, timeStamp: parseInt(payload.duration.toString())}]) return null } diff --git a/src/onebot11/action/SetGroupCard.ts b/src/onebot11/action/SetGroupCard.ts index d0bbefb..0905181 100644 --- a/src/onebot11/action/SetGroupCard.ts +++ b/src/onebot11/action/SetGroupCard.ts @@ -1,7 +1,7 @@ import BaseAction from "./BaseAction"; -import {NTQQApi} from "../../ntqqapi/ntcall"; import {getGroupMember} from "../../common/data"; import {ActionName} from "./types"; +import {NTQQGroupApi} from "../../ntqqapi/api/group"; interface Payload { group_id: number, @@ -17,7 +17,7 @@ export default class SetGroupCard extends BaseAction { if (!member) { throw `群成员${payload.user_id}不存在` } - await NTQQApi.setMemberCard(payload.group_id.toString(), member.uid, payload.card || "") + await NTQQGroupApi.setMemberCard(payload.group_id.toString(), member.uid, payload.card || "") return null } } \ No newline at end of file diff --git a/src/onebot11/action/SetGroupKick.ts b/src/onebot11/action/SetGroupKick.ts index ec5b71d..5491215 100644 --- a/src/onebot11/action/SetGroupKick.ts +++ b/src/onebot11/action/SetGroupKick.ts @@ -1,7 +1,7 @@ import BaseAction from "./BaseAction"; -import {NTQQApi} from "../../ntqqapi/ntcall"; import {getGroupMember} from "../../common/data"; import {ActionName} from "./types"; +import {NTQQGroupApi} from "../../ntqqapi/api/group"; interface Payload { group_id: number, @@ -17,7 +17,7 @@ export default class SetGroupKick extends BaseAction { if (!member) { throw `群成员${payload.user_id}不存在` } - await NTQQApi.kickMember(payload.group_id.toString(), [member.uid], !!payload.reject_add_request); + await NTQQGroupApi.kickMember(payload.group_id.toString(), [member.uid], !!payload.reject_add_request); return null } } \ No newline at end of file diff --git a/src/onebot11/action/SetGroupLeave.ts b/src/onebot11/action/SetGroupLeave.ts index 7986a2c..b623d35 100644 --- a/src/onebot11/action/SetGroupLeave.ts +++ b/src/onebot11/action/SetGroupLeave.ts @@ -1,7 +1,7 @@ import BaseAction from "./BaseAction"; -import {NTQQApi} from "../../ntqqapi/ntcall"; import {log} from "../../common/utils"; import {ActionName} from "./types"; +import {NTQQGroupApi} from "../../ntqqapi/api/group"; interface Payload { group_id: number, @@ -13,7 +13,7 @@ export default class SetGroupLeave extends BaseAction { protected async _handle(payload: Payload): Promise { try { - await NTQQApi.quitGroup(payload.group_id.toString()) + await NTQQGroupApi.quitGroup(payload.group_id.toString()) } catch (e) { log("退群失败", e) throw e diff --git a/src/onebot11/action/SetGroupName.ts b/src/onebot11/action/SetGroupName.ts index e22b472..e2efe3a 100644 --- a/src/onebot11/action/SetGroupName.ts +++ b/src/onebot11/action/SetGroupName.ts @@ -1,6 +1,6 @@ import BaseAction from "./BaseAction"; -import {NTQQApi} from "../../ntqqapi/ntcall"; import {ActionName} from "./types"; +import {NTQQGroupApi} from "../../ntqqapi/api/group"; interface Payload { group_id: number, @@ -12,7 +12,7 @@ export default class SetGroupName extends BaseAction { protected async _handle(payload: Payload): Promise { - await NTQQApi.setGroupName(payload.group_id.toString(), payload.group_name) + await NTQQGroupApi.setGroupName(payload.group_id.toString(), payload.group_name) return null } } \ No newline at end of file diff --git a/src/onebot11/action/SetGroupWholeBan.ts b/src/onebot11/action/SetGroupWholeBan.ts index a29de70..9ce2fe7 100644 --- a/src/onebot11/action/SetGroupWholeBan.ts +++ b/src/onebot11/action/SetGroupWholeBan.ts @@ -1,6 +1,6 @@ import BaseAction from "./BaseAction"; -import {NTQQApi} from "../../ntqqapi/ntcall"; import {ActionName} from "./types"; +import {NTQQGroupApi} from "../../ntqqapi/api/group"; interface Payload { group_id: number, @@ -12,7 +12,7 @@ export default class SetGroupWholeBan extends BaseAction { protected async _handle(payload: Payload): Promise { - await NTQQApi.banGroup(payload.group_id.toString(), !!payload.enable) + await NTQQGroupApi.banGroup(payload.group_id.toString(), !!payload.enable) return null } } \ No newline at end of file diff --git a/src/onebot11/action/go-cqhttp/UploadGroupFile.ts b/src/onebot11/action/go-cqhttp/UploadGroupFile.ts index 1cd0024..4eca434 100644 --- a/src/onebot11/action/go-cqhttp/UploadGroupFile.ts +++ b/src/onebot11/action/go-cqhttp/UploadGroupFile.ts @@ -3,9 +3,9 @@ import {getGroup} from "../../../common/data"; import {ActionName} from "../types"; import {SendMsgElementConstructor} from "../../../ntqqapi/constructor"; import {ChatType, SendFileElement} from "../../../ntqqapi/types"; -import {NTQQApi} from "../../../ntqqapi/ntcall"; import {uri2local} from "../../utils"; import fs from "fs"; +import {NTQQMsgApi} from "../../../ntqqapi/api/msg"; interface Payload{ group_id: number @@ -31,7 +31,7 @@ export default class GoCQHTTPUploadGroupFile extends BaseAction { throw new Error(downloadResult.errMsg) } let sendFileEle: SendFileElement = await SendMsgElementConstructor.file(downloadResult.path, payload.name); - await NTQQApi.sendMsg({chatType: ChatType.group, peerUid: group.groupCode}, [sendFileEle]); + await NTQQMsgApi.sendMsg({chatType: ChatType.group, peerUid: group.groupCode}, [sendFileEle]); return null } } \ No newline at end of file diff --git a/src/onebot11/action/llonebot/GetGroupAddRequest.ts b/src/onebot11/action/llonebot/GetGroupAddRequest.ts index 059c289..40d534c 100644 --- a/src/onebot11/action/llonebot/GetGroupAddRequest.ts +++ b/src/onebot11/action/llonebot/GetGroupAddRequest.ts @@ -1,9 +1,10 @@ import {GroupNotify, GroupNotifyStatus} from "../../../ntqqapi/types"; import BaseAction from "../BaseAction"; import {ActionName} from "../types"; -import {NTQQApi} from "../../../ntqqapi/ntcall"; import {uidMaps} from "../../../common/data"; import {log} from "../../../common/utils"; +import {NTQQUserApi} from "../../../ntqqapi/api/user"; +import {NTQQGroupApi} from "../../../ntqqapi/api/group"; interface OB11GroupRequestNotify { group_id: number, @@ -15,12 +16,12 @@ export default class GetGroupAddRequest extends BaseAction { - const data = await NTQQApi.getGroupIgnoreNotifies() + const data = await NTQQGroupApi.getGroupIgnoreNotifies() log(data); let notifies: GroupNotify[] = data.notifies.filter(notify => notify.status === GroupNotifyStatus.WAIT_HANDLE); let returnData: OB11GroupRequestNotify[] = [] for (const notify of notifies) { - const uin = uidMaps[notify.user1.uid] || (await NTQQApi.getUserDetailInfo(notify.user1.uid))?.uin + const uin = uidMaps[notify.user1.uid] || (await NTQQUserApi.getUserDetailInfo(notify.user1.uid))?.uin returnData.push({ group_id: parseInt(notify.group.groupCode), user_id: parseInt(uin), diff --git a/src/onebot11/action/llonebot/SetQQAvatar.ts b/src/onebot11/action/llonebot/SetQQAvatar.ts index 4c1ec28..fd02df6 100644 --- a/src/onebot11/action/llonebot/SetQQAvatar.ts +++ b/src/onebot11/action/llonebot/SetQQAvatar.ts @@ -1,9 +1,9 @@ import BaseAction from "../BaseAction"; -import {NTQQApi} from "../../../ntqqapi/ntcall"; import {ActionName} from "../types"; import { uri2local } from "../../utils"; import * as fs from "node:fs"; import { checkFileReceived } from "../../../common/utils"; +import {NTQQUserApi} from "../../../ntqqapi/api/user"; // import { log } from "../../../common/utils"; interface Payload { @@ -20,7 +20,7 @@ export default class SetAvatar extends BaseAction { } if (path) { await checkFileReceived(path, 5000); // 文件不存在QQ会崩溃,需要提前判断 - const ret = await NTQQApi.setQQAvatar(path) + const ret = await NTQQUserApi.setQQAvatar(path) if (!isLocal){ fs.unlink(path, () => {}) } diff --git a/src/onebot11/constructor.ts b/src/onebot11/constructor.ts index d55c5ce..57af42e 100644 --- a/src/onebot11/constructor.ts +++ b/src/onebot11/constructor.ts @@ -22,7 +22,6 @@ import { } from '../ntqqapi/types'; import {getFriend, getGroupMember, selfInfo, tempGroupCodeMap} from '../common/data'; import {getConfigUtil, log, sleep} from "../common/utils"; -import {NTQQApi} from "../ntqqapi/ntcall"; import {EventType} from "./event/OB11BaseEvent"; import {encodeCQCode} from "./cqcode"; import {dbUtil} from "../common/db"; @@ -30,6 +29,8 @@ import {OB11GroupIncreaseEvent} from "./event/notice/OB11GroupIncreaseEvent"; import {OB11GroupBanEvent} from "./event/notice/OB11GroupBanEvent"; import {OB11GroupUploadNoticeEvent} from "./event/notice/OB11GroupUploadNoticeEvent"; import {OB11GroupNoticeEvent} from "./event/notice/OB11GroupNoticeEvent"; +import {NTQQUserApi} from "../ntqqapi/api/user"; +import {NTQQFileApi} from "../ntqqapi/api/file"; export class OB11Constructor { @@ -144,7 +145,7 @@ export class OB11Constructor { fileSize: element.picElement.fileSize.toString(), url: message_data["data"]["url"], downloadFunc: async () => { - await NTQQApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid, + await NTQQFileApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid, element.elementId, element.picElement.thumbPath?.get(0) || "", element.picElement.sourcePath) } }).then() @@ -161,7 +162,7 @@ export class OB11Constructor { filePath: element.videoElement.filePath, fileSize: element.videoElement.fileSize, downloadFunc: async () => { - await NTQQApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid, + await NTQQFileApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid, element.elementId, element.videoElement.thumbPath.get(0), element.videoElement.filePath) } }).then() @@ -177,7 +178,7 @@ export class OB11Constructor { filePath: element.fileElement.filePath, fileSize: element.fileElement.fileSize, downloadFunc: async () => { - await NTQQApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid, + await NTQQFileApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid, element.elementId, null, element.fileElement.filePath) } }).then() @@ -235,7 +236,7 @@ export class OB11Constructor { const member = await getGroupMember(msg.peerUid, groupElement.memberUid); let memberUin = member?.uin; if (!memberUin) { - memberUin = (await NTQQApi.getUserDetailInfo(groupElement.memberUid)).uin + memberUin = (await NTQQUserApi.getUserDetailInfo(groupElement.memberUid)).uin } // log("获取新群成员QQ", memberUin) const adminMember = await getGroupMember(msg.peerUid, groupElement.adminUid); @@ -255,7 +256,7 @@ export class OB11Constructor { let duration = parseInt(groupElement.shutUp.duration) let sub_type: "ban" | "lift_ban" = duration > 0 ? "ban" : "lift_ban" if (memberUid){ - memberUin = (await getGroupMember(msg.peerUid, memberUid))?.uin || (await NTQQApi.getUserDetailInfo(memberUid))?.uin + memberUin = (await getGroupMember(msg.peerUid, memberUid))?.uin || (await NTQQUserApi.getUserDetailInfo(memberUid))?.uin } else { memberUin = "0"; // 0表示全员禁言 @@ -263,7 +264,7 @@ export class OB11Constructor { duration = -1 } } - const adminUin = (await getGroupMember(msg.peerUid, adminUid))?.uin || (await NTQQApi.getUserDetailInfo(adminUid))?.uin + const adminUin = (await getGroupMember(msg.peerUid, adminUid))?.uin || (await NTQQUserApi.getUserDetailInfo(adminUid))?.uin if (memberUin && adminUin) { return new OB11GroupBanEvent(parseInt(msg.peerUid), parseInt(memberUin), parseInt(adminUin), duration, sub_type); } diff --git a/src/version.ts b/src/version.ts index fe899f1..3a60d3f 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const version = "3.15.2" \ No newline at end of file +export const version = "3.15.3" \ No newline at end of file From 1a6739ffab7763d689d21eae4096db0edb77c571 Mon Sep 17 00:00:00 2001 From: linyuchen Date: Sun, 17 Mar 2024 11:35:38 +0800 Subject: [PATCH 4/5] refactor: ntqqapi types --- src/common/utils/qqlevel.ts | 7 + src/main/main.ts | 2 +- src/ntqqapi/api/msg.ts | 26 ++- src/ntqqapi/hook.ts | 6 +- src/ntqqapi/ntcall.ts | 1 + src/ntqqapi/types/cache.ts | 65 +++++++ src/ntqqapi/types/group.ts | 55 ++++++ src/ntqqapi/types/index.ts | 7 + src/ntqqapi/{types.ts => types/msg.ts} | 198 +--------------------- src/ntqqapi/types/notify.ts | 64 +++++++ src/ntqqapi/types/user.ts | 28 +++ src/onebot11/action/GetGroupMemberInfo.ts | 6 + src/onebot11/constructor.ts | 16 +- 13 files changed, 277 insertions(+), 204 deletions(-) create mode 100644 src/common/utils/qqlevel.ts create mode 100644 src/ntqqapi/types/cache.ts create mode 100644 src/ntqqapi/types/group.ts create mode 100644 src/ntqqapi/types/index.ts rename src/ntqqapi/{types.ts => types/msg.ts} (62%) create mode 100644 src/ntqqapi/types/notify.ts create mode 100644 src/ntqqapi/types/user.ts diff --git a/src/common/utils/qqlevel.ts b/src/common/utils/qqlevel.ts new file mode 100644 index 0000000..49cc13a --- /dev/null +++ b/src/common/utils/qqlevel.ts @@ -0,0 +1,7 @@ +// QQ等级换算 +import {QQLevel} from "../../ntqqapi/types"; + +export function calcQQLevel(level: QQLevel) { + const {crownNum, sunNum, moonNum, starNum} = level + return crownNum * 64 + sunNum * 16 + moonNum * 4 + starNum +} \ No newline at end of file diff --git a/src/main/main.ts b/src/main/main.ts index 06b4091..194c7df 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -131,7 +131,7 @@ function onLoad() { log("report message error: ", e.stack.toString()); } }) - registerReceiveHook<{ msgList: Array }>([ReceiveCmdS.UPDATE_MSG, ReceiveCmdS.UPDATE_ACTIVE_MSG], async (payload) => { + registerReceiveHook<{ msgList: Array }>([ReceiveCmdS.UPDATE_MSG], async (payload) => { for (const message of payload.msgList) { // log("message update", message.sendStatus, message.msgId, message.msgSeq) if (message.recallTime != "0") { //todo: 这个判断方法不太好,应该使用灰色消息元素来判断 diff --git a/src/ntqqapi/api/msg.ts b/src/ntqqapi/api/msg.ts index 9aa5a44..5721d53 100644 --- a/src/ntqqapi/api/msg.ts +++ b/src/ntqqapi/api/msg.ts @@ -4,6 +4,7 @@ import {log, sleep} from "../../common/utils"; import {dbUtil} from "../../common/db"; import {selfInfo} from "../../common/data"; import {ReceiveCmdS, registerReceiveHook} from "../hook"; + export let sendMessagePool: Record void) | null> = {}// peerUid: callbackFunnc export interface Peer { @@ -13,12 +14,33 @@ export interface Peer { } export class NTQQMsgApi { - static async activateChat(peer: Peer) { + static async activateGroupChat(groupCode: string) { return await callNTQQApi({ methodName: NTQQApiMethod.ADD_ACTIVE_CHAT, - args: [{peer, cnt: 20}] + args: [{peer:{peerUid: groupCode, chatType: ChatType.group}, cnt: 20}] }) } + static async fetchRecentContact(){ + await callNTQQApi({ + methodName: NTQQApiMethod.RECENT_CONTACT, + args: [ + { + fetchParam: { + anchorPointContact: { + contactId: '', + sortField: '', + pos: 0, + }, + relativeMoveCount: 0, + listType: 2, // 1普通消息,2群助手内的消息 + count: 200, + fetchOld: true, + }, + } + ] + }) + } + static async recallMsg(peer: Peer, msgIds: string[]) { return await callNTQQApi({ methodName: NTQQApiMethod.RECALL_MSG, diff --git a/src/ntqqapi/hook.ts b/src/ntqqapi/hook.ts index 0e4099e..18ba1d0 100644 --- a/src/ntqqapi/hook.ts +++ b/src/ntqqapi/hook.ts @@ -1,8 +1,8 @@ import {BrowserWindow} from 'electron'; import {getConfigUtil, log, sleep} from "../common/utils"; import {NTQQApiClass} from "./ntcall"; -import {sendMessagePool} from "./api/msg" -import {Group, RawMessage, User} from "./types"; +import {NTQQMsgApi, sendMessagePool} from "./api/msg" +import {ChatType, Group, RawMessage, User} from "./types"; import {friends, groups, selfInfo, tempGroupCodeMap} from "../common/data"; import {OB11GroupDecreaseEvent} from "../onebot11/event/notice/OB11GroupDecreaseEvent"; import {v4 as uuidv4} from "uuid" @@ -146,6 +146,8 @@ export function removeReceiveHook(id: string) { async function updateGroups(_groups: Group[], needUpdate: boolean = true) { for (let group of _groups) { + log("update group", group) + NTQQMsgApi.activateGroupChat(group.groupCode).then() let existGroup = groups.find(g => g.groupCode == group.groupCode); if (existGroup) { Object.assign(existGroup, group); diff --git a/src/ntqqapi/ntcall.ts b/src/ntqqapi/ntcall.ts index 9ffd3f7..b886459 100644 --- a/src/ntqqapi/ntcall.ts +++ b/src/ntqqapi/ntcall.ts @@ -15,6 +15,7 @@ export enum NTQQApiClass { } export enum NTQQApiMethod { + RECENT_CONTACT = "nodeIKernelRecentContactService/fetchAndSubscribeABatchOfRecentContact", ADD_ACTIVE_CHAT = "nodeIKernelMsgService/getAioFirstViewLatestMsgsAndAddActiveChat", // 激活群助手内的聊天窗口,这样才能收到消息 LIKE_FRIEND = "nodeIKernelProfileLikeService/setBuddyProfileLike", SELF_INFO = "fetchAuthData", diff --git a/src/ntqqapi/types/cache.ts b/src/ntqqapi/types/cache.ts new file mode 100644 index 0000000..e4407d7 --- /dev/null +++ b/src/ntqqapi/types/cache.ts @@ -0,0 +1,65 @@ +import {ChatType} from "./msg"; + +export interface CacheScanResult { + result: number, + size: [ // 单位为字节 + string, // 系统总存储空间 + string, // 系统可用存储空间 + string, // 系统已用存储空间 + string, // QQ总大小 + string, // 「聊天与文件」大小 + string, // 未知 + string, // 「缓存数据」大小 + string, // 「其他数据」大小 + string, // 未知 + ] +} + +export interface ChatCacheList { + pageCount: number, + infos: ChatCacheListItem[] +} + +export interface ChatCacheListItem { + chatType: ChatType, + basicChatCacheInfo: ChatCacheListItemBasic, + guildChatCacheInfo: unknown[] // TODO: 没用过频道所以不知道这里边的详细内容 +} + +export interface ChatCacheListItemBasic { + chatSize: string, + chatTime: string, + uid: string, + uin: string, + remarkName: string, + nickName: string, + chatType?: ChatType, + isChecked?: boolean +} + +export enum CacheFileType { + IMAGE = 0, + VIDEO = 1, + AUDIO = 2, + DOCUMENT = 3, + OTHER = 4, +} + +export interface CacheFileList { + infos: CacheFileListItem[], +} + +export interface CacheFileListItem { + fileSize: string, + fileTime: string, + fileKey: string, + elementId: string, + elementIdStr: string, + fileType: CacheFileType, + path: string, + fileName: string, + senderId: string, + previewPath: string, + senderName: string, + isChecked?: boolean, +} diff --git a/src/ntqqapi/types/group.ts b/src/ntqqapi/types/group.ts new file mode 100644 index 0000000..5e4b690 --- /dev/null +++ b/src/ntqqapi/types/group.ts @@ -0,0 +1,55 @@ +import {QQLevel, Sex} from "./user"; + +export interface Group { + groupCode: string, + maxMember: number, + memberCount: number, + groupName: string, + groupStatus: 0, + memberRole: 2, + isTop: boolean, + toppedTimestamp: "0", + privilegeFlag: number, //65760 + isConf: boolean, + hasModifyConfGroupFace: boolean, + hasModifyConfGroupName: boolean, + remarkName: string, + hasMemo: boolean, + groupShutupExpireTime: string, //"0", + personShutupExpireTime: string, //"0", + discussToGroupUin: string, //"0", + discussToGroupMaxMsgSeq: number, + discussToGroupTime: number, + groupFlagExt: number, //1073938496, + authGroupType: number, //0, + groupCreditLevel: number, //0, + groupFlagExt3: number, //0, + groupOwnerId: { + "memberUin": string, //"0", + "memberUid": string, //"u_fbf8N7aeuZEnUiJAbQ9R8Q" + }, + members: GroupMember[] // 原始数据是没有这个的,为了方便自己加了这个字段 +} + +export enum GroupMemberRole { + normal = 2, + admin = 3, + owner = 4 +} + +export interface GroupMember { + avatarPath: string; + cardName: string; + cardType: number; + isDelete: boolean; + nick: string; + qid: string; + remark: string; + role: GroupMemberRole; // 群主:4, 管理员:3,群员:2 + shutUpTime: number; // 禁言时间,单位是什么暂时不清楚 + uid: string; // 加密的字符串 + uin: string; // QQ号 + isRobot: boolean; + sex?: Sex + qqLevel?: QQLevel +} \ No newline at end of file diff --git a/src/ntqqapi/types/index.ts b/src/ntqqapi/types/index.ts new file mode 100644 index 0000000..dc69d2a --- /dev/null +++ b/src/ntqqapi/types/index.ts @@ -0,0 +1,7 @@ + +export * from './user'; +export * from './group'; +export * from './msg'; +export * from './notify'; +export * from './cache'; + diff --git a/src/ntqqapi/types.ts b/src/ntqqapi/types/msg.ts similarity index 62% rename from src/ntqqapi/types.ts rename to src/ntqqapi/types/msg.ts index 6618982..53e056d 100644 --- a/src/ntqqapi/types.ts +++ b/src/ntqqapi/types/msg.ts @@ -1,70 +1,4 @@ -export interface User { - uid: string; // 加密的字符串 - uin: string; // QQ号 - nick: string; - avatarUrl?: string; - longNick?: string; // 签名 - remark?: string -} - -export interface SelfInfo extends User { - online?: boolean; -} - -export interface Friend extends User { -} - -export interface Group { - groupCode: string, - maxMember: number, - memberCount: number, - groupName: string, - groupStatus: 0, - memberRole: 2, - isTop: boolean, - toppedTimestamp: "0", - privilegeFlag: number, //65760 - isConf: boolean, - hasModifyConfGroupFace: boolean, - hasModifyConfGroupName: boolean, - remarkName: string, - hasMemo: boolean, - groupShutupExpireTime: string, //"0", - personShutupExpireTime: string, //"0", - discussToGroupUin: string, //"0", - discussToGroupMaxMsgSeq: number, - discussToGroupTime: number, - groupFlagExt: number, //1073938496, - authGroupType: number, //0, - groupCreditLevel: number, //0, - groupFlagExt3: number, //0, - groupOwnerId: { - "memberUin": string, //"0", - "memberUid": string, //"u_fbf8N7aeuZEnUiJAbQ9R8Q" - }, - members: GroupMember[] // 原始数据是没有这个的,为了方便自己加了这个字段 -} - -export enum GroupMemberRole { - normal = 2, - admin = 3, - owner = 4 -} - -export interface GroupMember { - avatarPath: string; - cardName: string; - cardType: number; - isDelete: boolean; - nick: string; - qid: string; - remark: string; - role: GroupMemberRole; // 群主:4, 管理员:3,群员:2 - shutUpTime: number; // 禁言时间,单位是什么暂时不清楚 - uid: string; // 加密的字符串 - uin: string; // QQ号 - isRobot: boolean; -} +import {GroupMemberRole} from "./group"; export enum ElementType { TEXT = 1, @@ -383,132 +317,4 @@ export interface RawMessage { videoElement: VideoElement; fileElement: FileElement; }[]; -} - -export enum GroupNotifyTypes { - INVITE_ME = 1, - INVITED_JOIN = 4, // 有人接受了邀请入群 - JOIN_REQUEST = 7, - ADMIN_SET = 8, - ADMIN_UNSET = 12, - MEMBER_EXIT = 11, // 主动退出? - -} - -export interface GroupNotifies { - doubt: boolean, - nextStartSeq: string, - notifies: GroupNotify[], -} - -export enum GroupNotifyStatus { - IGNORE = 0, - WAIT_HANDLE = 1, - APPROVE = 2, - REJECT = 3 -} - -export interface GroupNotify { - time: number; // 自己添加的字段,时间戳,毫秒, 用于判断收到短时间内收到重复的notify - seq: string, // 唯一标识符,转成数字再除以1000应该就是时间戳? - type: GroupNotifyTypes, - status: GroupNotifyStatus, // 0是已忽略?,1是未处理,2是已同意 - group: { groupCode: string, groupName: string }, - user1: { uid: string, nickName: string }, // 被设置管理员的人 - user2: { uid: string, nickName: string }, // 操作者 - actionUser: { uid: string, nickName: string }, //未知 - actionTime: string, - invitationExt: { - srcType: number, // 0?未知 - groupCode: string, waitStatus: number - }, - postscript: string, // 加群用户填写的验证信息 - repeatSeqs: [], - warningTips: string -} - -export enum GroupRequestOperateTypes { - approve = 1, - reject = 2 -} - -export interface FriendRequest { - friendUid: string, - reqTime: string, // 时间戳,秒 - extWords: string, // 申请人填写的验证消息 - isUnread: boolean, - friendNick: string, - sourceId: number, - groupCode: string -} - -export interface FriendRequestNotify { - data: { - unreadNums: number, - buddyReqs: FriendRequest[] - } -} - -export interface CacheScanResult { - result: number, - size: [ // 单位为字节 - string, // 系统总存储空间 - string, // 系统可用存储空间 - string, // 系统已用存储空间 - string, // QQ总大小 - string, // 「聊天与文件」大小 - string, // 未知 - string, // 「缓存数据」大小 - string, // 「其他数据」大小 - string, // 未知 - ] -} - -export interface ChatCacheList { - pageCount: number, - infos: ChatCacheListItem[] -} - -export interface ChatCacheListItem { - chatType: ChatType, - basicChatCacheInfo: ChatCacheListItemBasic, - guildChatCacheInfo: unknown[] // TODO: 没用过频道所以不知道这里边的详细内容 -} - -export interface ChatCacheListItemBasic { - chatSize: string, - chatTime: string, - uid: string, - uin: string, - remarkName: string, - nickName: string, - chatType?: ChatType, - isChecked?: boolean -} - -export enum CacheFileType { - IMAGE = 0, - VIDEO = 1, - AUDIO = 2, - DOCUMENT = 3, - OTHER = 4, -} - -export interface CacheFileList { - infos: CacheFileListItem[], -} - -export interface CacheFileListItem { - fileSize: string, - fileTime: string, - fileKey: string, - elementId: string, - elementIdStr: string, - fileType: CacheFileType, - path: string, - fileName: string, - senderId: string, - previewPath: string, - senderName: string, - isChecked?: boolean, -} +} \ No newline at end of file diff --git a/src/ntqqapi/types/notify.ts b/src/ntqqapi/types/notify.ts new file mode 100644 index 0000000..29874fe --- /dev/null +++ b/src/ntqqapi/types/notify.ts @@ -0,0 +1,64 @@ + +export enum GroupNotifyTypes { + INVITE_ME = 1, + INVITED_JOIN = 4, // 有人接受了邀请入群 + JOIN_REQUEST = 7, + ADMIN_SET = 8, + ADMIN_UNSET = 12, + MEMBER_EXIT = 11, // 主动退出? + +} + +export interface GroupNotifies { + doubt: boolean, + nextStartSeq: string, + notifies: GroupNotify[], +} + +export enum GroupNotifyStatus { + IGNORE = 0, + WAIT_HANDLE = 1, + APPROVE = 2, + REJECT = 3 +} + +export interface GroupNotify { + time: number; // 自己添加的字段,时间戳,毫秒, 用于判断收到短时间内收到重复的notify + seq: string, // 唯一标识符,转成数字再除以1000应该就是时间戳? + type: GroupNotifyTypes, + status: GroupNotifyStatus, // 0是已忽略?,1是未处理,2是已同意 + group: { groupCode: string, groupName: string }, + user1: { uid: string, nickName: string }, // 被设置管理员的人 + user2: { uid: string, nickName: string }, // 操作者 + actionUser: { uid: string, nickName: string }, //未知 + actionTime: string, + invitationExt: { + srcType: number, // 0?未知 + groupCode: string, waitStatus: number + }, + postscript: string, // 加群用户填写的验证信息 + repeatSeqs: [], + warningTips: string +} + +export enum GroupRequestOperateTypes { + approve = 1, + reject = 2 +} + +export interface FriendRequest { + friendUid: string, + reqTime: string, // 时间戳,秒 + extWords: string, // 申请人填写的验证消息 + isUnread: boolean, + friendNick: string, + sourceId: number, + groupCode: string +} + +export interface FriendRequestNotify { + data: { + unreadNums: number, + buddyReqs: FriendRequest[] + } +} diff --git a/src/ntqqapi/types/user.ts b/src/ntqqapi/types/user.ts new file mode 100644 index 0000000..891b8a4 --- /dev/null +++ b/src/ntqqapi/types/user.ts @@ -0,0 +1,28 @@ +export enum Sex { + male = 0, + female = 2, + unknown = 255, +} + +export interface QQLevel { + "crownNum": number, + "sunNum": number, + "moonNum": number, + "starNum": number +} +export interface User { + uid: string; // 加密的字符串 + uin: string; // QQ号 + nick: string; + avatarUrl?: string; + longNick?: string; // 签名 + remark?: string; + sex?: Sex; + "qqLevel"?: QQLevel +} + +export interface SelfInfo extends User { + online?: boolean; +} + +export interface Friend extends User {} \ No newline at end of file diff --git a/src/onebot11/action/GetGroupMemberInfo.ts b/src/onebot11/action/GetGroupMemberInfo.ts index eb541c5..9e43e4c 100644 --- a/src/onebot11/action/GetGroupMemberInfo.ts +++ b/src/onebot11/action/GetGroupMemberInfo.ts @@ -3,6 +3,8 @@ import {getGroupMember} from "../../common/data"; import {OB11Constructor} from "../constructor"; import BaseAction from "./BaseAction"; import {ActionName} from "./types"; +import {NTQQUserApi} from "../../ntqqapi/api/user"; +import {isNull, log} from "../../common/utils"; export interface PayloadType { @@ -16,6 +18,10 @@ class GetGroupMemberInfo extends BaseAction { protected async _handle(payload: PayloadType) { const member = await getGroupMember(payload.group_id.toString(), payload.user_id.toString()) if (member) { + if (isNull(member.sex)){ + let info = (await NTQQUserApi.getUserDetailInfo(member.uid)) + Object.assign(member, info); + } return OB11Constructor.groupMember(payload.group_id.toString(), member) } else { throw (`群成员${payload.user_id}不存在`) diff --git a/src/onebot11/constructor.ts b/src/onebot11/constructor.ts index 57af42e..8aff3b0 100644 --- a/src/onebot11/constructor.ts +++ b/src/onebot11/constructor.ts @@ -16,7 +16,7 @@ import { GroupMember, IMAGE_HTTP_HOST, RawMessage, - SelfInfo, + SelfInfo, Sex, TipGroupElementType, User } from '../ntqqapi/types'; @@ -31,6 +31,7 @@ import {OB11GroupUploadNoticeEvent} from "./event/notice/OB11GroupUploadNoticeEv import {OB11GroupNoticeEvent} from "./event/notice/OB11GroupNoticeEvent"; import {NTQQUserApi} from "../ntqqapi/api/user"; import {NTQQFileApi} from "../ntqqapi/api/file"; +import {calcQQLevel} from "../common/utils/qqlevel"; export class OB11Constructor { @@ -225,6 +226,7 @@ export class OB11Constructor { if (msg.chatType !== ChatType.group) { return; } + // log("group msg", msg); for (let element of msg.elements) { const grayTipElement = element.grayTipElement const groupElement = grayTipElement?.groupElement @@ -325,16 +327,24 @@ export class OB11Constructor { }[role] } + static sex(sex: Sex): OB11UserSex{ + const sexMap = { + [Sex.male]: OB11UserSex.male, + [Sex.female]: OB11UserSex.female, + [Sex.unknown]: OB11UserSex.unknown + } + return sexMap[sex] || OB11UserSex.unknown + } static groupMember(group_id: string, member: GroupMember): OB11GroupMember { return { group_id: parseInt(group_id), user_id: parseInt(member.uin), nickname: member.nick, card: member.cardName, - sex: OB11UserSex.unknown, + sex: OB11Constructor.sex(member.sex), age: 0, area: "", - level: 0, + level: member.qqLevel && calcQQLevel(member.qqLevel) || 0, join_time: 0, // 暂时没法获取 last_sent_time: 0, // 暂时没法获取 title_expire_time: 0, From 1e144a13770d409329f33dbc0f3d5cf5af56c59c Mon Sep 17 00:00:00 2001 From: linyuchen Date: Sun, 17 Mar 2024 15:33:56 +0800 Subject: [PATCH 5/5] feat: cc poke --- electron.vite.config.ts | 24 +++++++------- manifest.json | 4 +-- src/common/config.ts | 2 +- src/main/main.ts | 13 ++++++++ src/ntqqapi/api/msg.ts | 4 ++- src/ntqqapi/constructor.ts | 6 ++-- src/ntqqapi/external/ccpoke/index.ts | 28 ++++++++++++++++ .../external/ccpoke/poke-win32-x64.node | Bin 0 -> 65536 bytes src/ntqqapi/hook.ts | 14 ++++++-- src/ntqqapi/ntcall.ts | 1 + src/onebot11/action/GetGroupMemberInfo.ts | 2 ++ src/onebot11/action/SendMsg.ts | 12 +++---- src/onebot11/event/notice/OB11PokeEvent.ts | 31 ++++++++++++++++++ src/onebot11/types.ts | 3 ++ src/renderer/index.ts | 4 +-- src/version.ts | 2 +- 16 files changed, 119 insertions(+), 31 deletions(-) create mode 100644 src/ntqqapi/external/ccpoke/index.ts create mode 100644 src/ntqqapi/external/ccpoke/poke-win32-x64.node create mode 100644 src/onebot11/event/notice/OB11PokeEvent.ts diff --git a/electron.vite.config.ts b/electron.vite.config.ts index 26525e0..1139273 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -6,7 +6,7 @@ const external = ["silk-wasm", "ws", "module-error", "catering", "node-gyp-build"]; function genCpModule(module: string) { - return { src: `./node_modules/${module}`, dest: `dist/node_modules/${module}`, flatten: false } + return {src: `./node_modules/${module}`, dest: `dist/node_modules/${module}`, flatten: false} } let config = { @@ -16,20 +16,24 @@ let config = { emptyOutDir: true, lib: { formats: ["cjs"], - entry: { "main": "src/main/main.ts" }, + entry: {"main": "src/main/main.ts"}, }, rollupOptions: { external, input: "src/main/main.ts", } }, - resolve:{ + resolve: { alias: { './lib-cov/fluent-ffmpeg': './lib/fluent-ffmpeg' }, }, - plugins: [cp({ targets: [...external.map(genCpModule), - { src: './manifest.json', dest: 'dist' }, {src: './icon.jpg', dest: 'dist' }] + plugins: [cp({ + targets: [ + ...external.map(genCpModule), + {src: './manifest.json', dest: 'dist'}, {src: './icon.jpg', dest: 'dist'}, + {src: './src/ntqqapi/external/ccpoke/poke-win32-x64.node', dest: 'dist/main/ccpoke/'}, + ] })] }, preload: { @@ -39,15 +43,14 @@ let config = { emptyOutDir: true, lib: { formats: ["cjs"], - entry: { "preload": "src/preload.ts" }, + entry: {"preload": "src/preload.ts"}, }, rollupOptions: { // external: externalAll, input: "src/preload.ts", } }, - resolve:{ - } + resolve: {} }, renderer: { // vite config options @@ -56,15 +59,14 @@ let config = { emptyOutDir: true, lib: { formats: ["es"], - entry: { "renderer": "src/renderer/index.ts" }, + entry: {"renderer": "src/renderer/index.ts"}, }, rollupOptions: { // external: externalAll, input: "src/renderer/index.ts", } }, - resolve:{ - } + resolve: {} } } diff --git a/manifest.json b/manifest.json index 9c887d8..c94e72e 100644 --- a/manifest.json +++ b/manifest.json @@ -1,10 +1,10 @@ { "manifest_version": 4, "type": "extension", - "name": "LLOneBot v3.15.3", + "name": "LLOneBot v3.16.0", "slug": "LLOneBot", "description": "LiteLoaderQQNT的OneBotApi,不支持商店在线更新", - "version": "3.15.3", + "version": "3.16.0", "icon": "./icon.jpg", "authors": [ { diff --git a/src/common/config.ts b/src/common/config.ts index 5a7a964..397c77c 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -2,7 +2,7 @@ import fs from "fs"; import {Config, OB11Config} from './types'; import {mergeNewProperties} from "./utils"; -export const HOOK_LOG = true; +export const HOOK_LOG = false; export const ALLOW_SEND_TEMP_MSG = false; diff --git a/src/main/main.ts b/src/main/main.ts index 194c7df..d0f8179 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -37,6 +37,8 @@ import {dbUtil} from "../common/db"; import {setConfig} from "./setConfig"; import {NTQQUserApi} from "../ntqqapi/api/user"; import {NTQQGroupApi} from "../ntqqapi/api/group"; +import {registerPokeHandler} from "../ntqqapi/external/ccpoke"; +import {OB11FriendPokeEvent, OB11GroupPokeEvent} from "../onebot11/event/notice/OB11PokeEvent"; let running = false; @@ -124,6 +126,16 @@ function onLoad() { } async function startReceiveHook() { + registerPokeHandler((id, isGroup) => { + log(`收到戳一戳消息了!是否群聊:${isGroup},id:${id}`) + let pokeEvent: OB11FriendPokeEvent | OB11GroupPokeEvent; + if (isGroup) { + pokeEvent = new OB11GroupPokeEvent(parseInt(id)); + }else{ + pokeEvent = new OB11FriendPokeEvent(parseInt(id)); + } + postOB11Event(pokeEvent); + }) registerReceiveHook<{ msgList: Array }>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], async (payload) => { try { await postReceiveMsg(payload.msgList); @@ -297,6 +309,7 @@ function onLoad() { async function start() { log("llonebot pid", process.pid) + startTime = Date.now(); startReceiveHook().then(); NTQQGroupApi.getGroups(true).then() diff --git a/src/ntqqapi/api/msg.ts b/src/ntqqapi/api/msg.ts index 5721d53..edbfa6b 100644 --- a/src/ntqqapi/api/msg.ts +++ b/src/ntqqapi/api/msg.ts @@ -15,7 +15,9 @@ export interface Peer { export class NTQQMsgApi { static async activateGroupChat(groupCode: string) { - return await callNTQQApi({ + // await this.fetchRecentContact(); + // await sleep(500); + return await callNTQQApi({ methodName: NTQQApiMethod.ADD_ACTIVE_CHAT, args: [{peer:{peerUid: groupCode, chatType: ChatType.group}, cnt: 20}] }) diff --git a/src/ntqqapi/constructor.ts b/src/ntqqapi/constructor.ts index 342c437..af6fea1 100644 --- a/src/ntqqapi/constructor.ts +++ b/src/ntqqapi/constructor.ts @@ -59,7 +59,7 @@ export class SendMsgElementConstructor { } } - static async pic(picPath: string): Promise { + static async pic(picPath: string, summary: string = ""): Promise { const {md5, fileName, path, fileSize} = await NTQQFileApi.uploadFile(picPath, ElementType.PIC); if (fileSize === 0) { throw "文件异常,大小为0"; @@ -78,7 +78,7 @@ export class SendMsgElementConstructor { fileUuid: "", fileSubId: "", thumbFileSize: 0, - summary: "", + summary, }; return { @@ -136,7 +136,7 @@ export class SendMsgElementConstructor { folder: thumb, size: videoInfo.width + "x" + videoInfo.height }).on("end", () => { - resolve(pathLib.join(thumb, thumbFileName)); + resolve(pathLib.join(thumb, thumbFileName)); }); }) let thumbPath = new Map() diff --git a/src/ntqqapi/external/ccpoke/index.ts b/src/ntqqapi/external/ccpoke/index.ts new file mode 100644 index 0000000..22f82d1 --- /dev/null +++ b/src/ntqqapi/external/ccpoke/index.ts @@ -0,0 +1,28 @@ +import {log} from "../../../common/utils"; + +let pokeEngine: any = null + +type PokeHandler = (id: string, isGroup: boolean)=>void + +let pokeRecords: Record = {} +export function registerPokeHandler(handler: PokeHandler){ + if(!pokeEngine){ + try { + pokeEngine = require("./ccpoke/poke-win32-x64.node") + pokeEngine.performHooks(); + }catch (e) { + log("戳一戳引擎加载失败", e) + return + } + } + pokeEngine.setHandlerForPokeHook((id: string, isGroup: boolean)=>{ + let existTime = pokeRecords[id] + if (existTime){ + if (Date.now() - existTime < 1500){ + return + } + } + pokeRecords[id] = Date.now() + handler(id, isGroup); + }) +} \ No newline at end of file diff --git a/src/ntqqapi/external/ccpoke/poke-win32-x64.node b/src/ntqqapi/external/ccpoke/poke-win32-x64.node new file mode 100644 index 0000000000000000000000000000000000000000..581f88e019bbc3707900170a9f75f8b38050f8ab GIT binary patch literal 65536 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 { + activatedGroups.push(group.groupCode); + // log(`激活群聊天窗口${group.groupName}(${group.groupCode})`, r) + // if (r.result !== 0) { + // setTimeout(() => NTQQMsgApi.activateGroupChat(group.groupCode).then(r => log(`再次激活群聊天窗口${group.groupName}(${group.groupCode})`, r)), 500); + // }else { + // } + }).catch(log) + } let existGroup = groups.find(g => g.groupCode == group.groupCode); if (existGroup) { Object.assign(existGroup, group); diff --git a/src/ntqqapi/ntcall.ts b/src/ntqqapi/ntcall.ts index b886459..98bc0c3 100644 --- a/src/ntqqapi/ntcall.ts +++ b/src/ntqqapi/ntcall.ts @@ -17,6 +17,7 @@ export enum NTQQApiClass { export enum NTQQApiMethod { RECENT_CONTACT = "nodeIKernelRecentContactService/fetchAndSubscribeABatchOfRecentContact", ADD_ACTIVE_CHAT = "nodeIKernelMsgService/getAioFirstViewLatestMsgsAndAddActiveChat", // 激活群助手内的聊天窗口,这样才能收到消息 + ADD_ACTIVE_CHAT_2 = "nodeIKernelMsgService/getMsgsIncludeSelfAndAddActiveChat", LIKE_FRIEND = "nodeIKernelProfileLikeService/setBuddyProfileLike", SELF_INFO = "fetchAuthData", FRIENDS = "nodeIKernelBuddyService/getBuddyList", diff --git a/src/onebot11/action/GetGroupMemberInfo.ts b/src/onebot11/action/GetGroupMemberInfo.ts index 9e43e4c..a4bcfda 100644 --- a/src/onebot11/action/GetGroupMemberInfo.ts +++ b/src/onebot11/action/GetGroupMemberInfo.ts @@ -19,7 +19,9 @@ class GetGroupMemberInfo extends BaseAction { const member = await getGroupMember(payload.group_id.toString(), payload.user_id.toString()) if (member) { if (isNull(member.sex)){ + log("获取群成员详细信息") let info = (await NTQQUserApi.getUserDetailInfo(member.uid)) + log("群成员详细信息结果", info) Object.assign(member, info); } return OB11Constructor.groupMember(payload.group_id.toString(), member) diff --git a/src/onebot11/action/SendMsg.ts b/src/onebot11/action/SendMsg.ts index 1784640..5375d21 100644 --- a/src/onebot11/action/SendMsg.ts +++ b/src/onebot11/action/SendMsg.ts @@ -417,20 +417,16 @@ export class SendMsg extends BaseAction { if (!isLocal) { // 只删除http和base64转过来的文件 deleteAfterSentFiles.push(path) } - const constructorMap = { - [OB11MessageDataType.image]: SendMsgElementConstructor.pic, - [OB11MessageDataType.voice]: SendMsgElementConstructor.ptt, - [OB11MessageDataType.video]: SendMsgElementConstructor.video, - [OB11MessageDataType.file]: SendMsgElementConstructor.file, - } if (sendMsg.type === OB11MessageDataType.file) { log("发送文件", path, payloadFileName || fileName) sendElements.push(await SendMsgElementConstructor.file(path, payloadFileName || fileName)); } else if (sendMsg.type === OB11MessageDataType.video) { log("发送视频", path, payloadFileName || fileName) sendElements.push(await SendMsgElementConstructor.video(path, payloadFileName || fileName)); - } else { - sendElements.push(await constructorMap[sendMsg.type](path)); + } else if (sendMsg.type === OB11MessageDataType.voice) { + sendElements.push(await SendMsgElementConstructor.ptt(path)); + }else if (sendMsg.type === OB11MessageDataType.image) { + sendElements.push(await SendMsgElementConstructor.pic(path, sendMsg.data.summary || "")); } } } diff --git a/src/onebot11/event/notice/OB11PokeEvent.ts b/src/onebot11/event/notice/OB11PokeEvent.ts new file mode 100644 index 0000000..0c81a96 --- /dev/null +++ b/src/onebot11/event/notice/OB11PokeEvent.ts @@ -0,0 +1,31 @@ +import {OB11BaseNoticeEvent} from "./OB11BaseNoticeEvent"; +import {selfInfo} from "../../../common/data"; +import {OB11BaseEvent} from "../OB11BaseEvent"; + +class OB11PokeEvent extends OB11BaseNoticeEvent{ + notice_type = "notify" + sub_type = "poke" + target_id = parseInt(selfInfo.uin) + user_id: number + +} + +export class OB11FriendPokeEvent extends OB11PokeEvent{ + sender_id: number + constructor(user_id: number) { + super(); + this.user_id = user_id; + this.sender_id = user_id; + } +} + +export class OB11GroupPokeEvent extends OB11PokeEvent{ + group_id: number + + constructor(group_id: number, user_id: number=0) { + super(); + this.group_id = group_id; + this.target_id = user_id; + this.user_id = user_id; + } +} diff --git a/src/onebot11/types.ts b/src/onebot11/types.ts index ed47253..581c87a 100644 --- a/src/onebot11/types.ts +++ b/src/onebot11/types.ts @@ -121,6 +121,9 @@ interface OB11MessageFileBase { export interface OB11MessageImage extends OB11MessageFileBase { type: OB11MessageDataType.image + data: OB11MessageFileBase['data'] & { + summary ? : string; // 图片摘要 + } } export interface OB11MessageRecord extends OB11MessageFileBase { diff --git a/src/renderer/index.ts b/src/renderer/index.ts index af26786..9399099 100644 --- a/src/renderer/index.ts +++ b/src/renderer/index.ts @@ -90,8 +90,8 @@ async function onSettingWindowCreated(view: Element) { ], 'ob11.messagePostFormat', config.ob11.messagePostFormat), ), SettingItem( - 'ffmpeg 路径,发送语音、视频需要,同时保证ffprobe和ffmpeg在一起', `配置可参考 官方文档 路径:${!isEmpty(config.ffmpeg) ? config.ffmpeg : '未指定'}`, - SettingButton('选择', 'config-ffmpeg-select'), + 'ffmpeg 路径,发送语音、视频需要,同时保证ffprobe和ffmpeg在一起', ` 下载地址 , 路径:${!isEmpty(config.ffmpeg) ? config.ffmpeg : '未指定'}`, + SettingButton('选择ffmpeg', 'config-ffmpeg-select'), ), SettingItem( '', null, diff --git a/src/version.ts b/src/version.ts index 3a60d3f..93bafb5 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const version = "3.15.3" \ No newline at end of file +export const version = "3.16.0" \ No newline at end of file