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 31edae9..c94e72e 100644 --- a/manifest.json +++ b/manifest.json @@ -1,10 +1,10 @@ { "manifest_version": 4, "type": "extension", - "name": "LLOneBot v3.15.1", + "name": "LLOneBot v3.16.0", "slug": "LLOneBot", "description": "LiteLoaderQQNT的OneBotApi,不支持商店在线更新", - "version": "3.15.1", + "version": "3.16.0", "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/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/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 2142073..e6c8b25 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -1,8 +1,8 @@ // 运行在 Electron 主进程 下的插件入口 -import { BrowserWindow, dialog, ipcMain } from 'electron'; +import {BrowserWindow, dialog, ipcMain} from 'electron'; import * as fs from 'node:fs'; -import { Config } from "../common/types"; +import {Config} from "../common/types"; import { CHANNEL_ERROR, CHANNEL_GET_CONFIG, @@ -12,8 +12,8 @@ import { CHANNEL_SET_CONFIG, CHANNEL_UPDATE, } from "../common/channels"; -import { ob11WebsocketServer } from "../onebot11/server/ws/WebsocketServer"; -import { checkFfmpeg, checkVersion, DATA_DIR, getConfigUtil, getRemoteVersion, log, updateLLOneBot } from "../common/utils"; +import {ob11WebsocketServer} from "../onebot11/server/ws/WebsocketServer"; +import {checkFfmpeg, DATA_DIR, getConfigUtil, log} from "../common/utils"; import { friendRequests, getFriend, @@ -23,21 +23,24 @@ import { refreshGroupMembers, selfInfo } from "../common/data"; -import { hookNTQQApiCall, hookNTQQApiReceive, ReceiveCmd, 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"; -import { OB11GroupRecallNoticeEvent } from "../onebot11/event/notice/OB11GroupRecallNoticeEvent"; -import { postOB11Event } from "../onebot11/server/postOB11Event"; -import { ob11ReverseWebsockets } from "../onebot11/server/ws/ReverseWebsocket"; -import { OB11GroupAdminNoticeEvent } from "../onebot11/event/notice/OB11GroupAdminNoticeEvent"; -import { OB11GroupRequestEvent } from "../onebot11/event/request/OB11GroupRequest"; -import { OB11FriendRequestEvent } from "../onebot11/event/request/OB11FriendRequest"; +import {hookNTQQApiCall, hookNTQQApiReceive, ReceiveCmdS, registerReceiveHook} from "../ntqqapi/hook"; +import {OB11Constructor} from "../onebot11/constructor"; +import {ChatType, FriendRequestNotify, GroupNotifies, GroupNotifyTypes, RawMessage} from "../ntqqapi/types"; +import {ob11HTTPServer} from "../onebot11/server/http"; +import {OB11FriendRecallNoticeEvent} from "../onebot11/event/notice/OB11FriendRecallNoticeEvent"; +import {OB11GroupRecallNoticeEvent} from "../onebot11/event/notice/OB11GroupRecallNoticeEvent"; +import {postOB11Event} from "../onebot11/server/postOB11Event"; +import {ob11ReverseWebsockets} from "../onebot11/server/ws/ReverseWebsocket"; +import {OB11GroupAdminNoticeEvent} from "../onebot11/event/notice/OB11GroupAdminNoticeEvent"; +import {OB11GroupRequestEvent} from "../onebot11/event/request/OB11GroupRequest"; +import {OB11FriendRequestEvent} from "../onebot11/event/request/OB11FriendRequest"; import * as path from "node:path"; -import { dbUtil } from "../common/db"; -import { setConfig } from "./setConfig"; +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; @@ -83,7 +86,7 @@ function onLoad() { } }) if (!fs.existsSync(DATA_DIR)) { - fs.mkdirSync(DATA_DIR, { recursive: true }); + fs.mkdirSync(DATA_DIR, {recursive: true}); } ipcMain.handle(CHANNEL_ERROR, (event, arg) => { return llonebotError; @@ -101,7 +104,7 @@ function onLoad() { }) async function postReceiveMsg(msgList: RawMessage[]) { - const { debug, reportSelfMessage } = getConfigUtil().getConfig(); + const {debug, reportSelfMessage} = getConfigUtil().getConfig(); for (let message of msgList) { // log("收到新消息", message.msgId, message.msgSeq) @@ -130,14 +133,24 @@ function onLoad() { } async function startReceiveHook() { - registerReceiveHook<{ msgList: Array }>(ReceiveCmd.NEW_MSG, async (payload) => { + 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); } catch (e) { log("report message error: ", e.stack.toString()); } }) - registerReceiveHook<{ msgList: Array }>(ReceiveCmd.UPDATE_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: 这个判断方法不太好,应该使用灰色消息元素来判断 @@ -173,8 +186,8 @@ function onLoad() { dbUtil.updateMsg(message).then(); } }) - registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmd.SELF_SEND_MSG, async (payload) => { - const { reportSelfMessage } = getConfigUtil().getConfig(); + registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, async (payload) => { + const {reportSelfMessage} = getConfigUtil().getConfig(); if (!reportSelfMessage) { return } 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..edbfa6b --- /dev/null +++ b/src/ntqqapi/api/msg.ts @@ -0,0 +1,187 @@ +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 activateGroupChat(groupCode: string) { + // await this.fetchRecentContact(); + // await sleep(500); + return await callNTQQApi({ + methodName: NTQQApiMethod.ADD_ACTIVE_CHAT, + 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, + 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..af6fea1 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 { @@ -59,12 +59,12 @@ export class SendMsgElementConstructor { } } - static async pic(picPath: string): Promise { - const {md5, fileName, path, fileSize} = await NTQQApi.uploadFile(picPath, ElementType.PIC); + static async pic(picPath: string, summary: string = ""): Promise { + 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, @@ -78,7 +78,7 @@ export class SendMsgElementConstructor { fileUuid: "", fileSubId: "", thumbFileSize: 0, - summary: "", + summary, }; return { @@ -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"; } @@ -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() @@ -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/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 0000000..581f88e Binary files /dev/null and b/src/ntqqapi/external/ccpoke/poke-win32-x64.node differ diff --git a/src/ntqqapi/hook.ts b/src/ntqqapi/hook.ts index c50f6d7..f2045f0 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 {Group, RawMessage, User} from "./types"; +import {NTQQApiClass} from "./ntcall"; +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 {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 }> = [] @@ -54,13 +59,19 @@ 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; // 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) @@ -98,7 +109,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); }, }); @@ -109,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, @@ -124,8 +144,20 @@ export function removeReceiveHook(id: string) { receiveHooks.splice(index, 1); } +let activatedGroups: string[] = []; async function updateGroups(_groups: Group[], needUpdate: boolean = true) { for (let group of _groups) { + // log("update group", group) + if (!activatedGroups.includes(group.groupCode)) { + NTQQMsgApi.activateGroupChat(group.groupCode).then((r) => { + 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); @@ -135,7 +167,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; @@ -154,7 +186,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(); // 建立索引降低时间复杂度 @@ -181,7 +213,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 { @@ -190,7 +222,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 { @@ -203,7 +235,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) { @@ -218,7 +250,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 @@ -241,7 +273,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; } @@ -258,7 +290,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); @@ -274,6 +306,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..98bc0c3 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,9 @@ export enum NTQQApiClass { } export enum NTQQApiMethod { - SET_HEADER = "nodeIKernelProfileService/setHeader", + 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", @@ -106,12 +74,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 +86,7 @@ interface NTQQApiParams { timeoutSecond?: number, } -function callNTQQApi(params: NTQQApiParams) { +export function callNTQQApi(params: NTQQApiParams) { let { className, methodName, channel, args, cbCmd, timeoutSecond: timeout, @@ -199,574 +161,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 +178,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/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 743b6df..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, @@ -178,6 +112,7 @@ export interface SendVideoElement { elementId: "", videoElement: VideoElement } + export interface SendArkElement { elementType: ElementType.ARK, elementId: "", @@ -243,7 +178,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 +193,10 @@ export interface GrayTipElement { wording: string; // 自定义的撤回提示语 } aioOpGrayTipElement: TipAioOpGrayTipElement, - groupElement: TipGroupElement + groupElement: TipGroupElement, + xmlElement: { + content: string; + } } export interface FaceElement { @@ -374,131 +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/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/GetGroupMemberInfo.ts b/src/onebot11/action/GetGroupMemberInfo.ts index eb541c5..a4bcfda 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,12 @@ 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)){ + log("获取群成员详细信息") + let info = (await NTQQUserApi.getUserDetailInfo(member.uid)) + log("群成员详细信息结果", info) + Object.assign(member, info); + } return OB11Constructor.groupMember(payload.group_id.toString(), member) } else { throw (`群成员${payload.user_id}不存在`) 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..5375d21 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; @@ -416,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 || "")); } } } @@ -449,7 +446,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 02d3b96..8aff3b0 100644 --- a/src/onebot11/constructor.ts +++ b/src/onebot11/constructor.ts @@ -11,17 +11,17 @@ import { import { AtType, ChatType, + GrayTipElementSubType, Group, GroupMember, IMAGE_HTTP_HOST, RawMessage, - SelfInfo, + SelfInfo, Sex, 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"; import {encodeCQCode} from "./cqcode"; import {dbUtil} from "../common/db"; @@ -29,6 +29,9 @@ 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"; +import {calcQQLevel} from "../common/utils/qqlevel"; export class OB11Constructor { @@ -143,7 +146,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() @@ -160,7 +163,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() @@ -176,7 +179,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() @@ -206,33 +209,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') { @@ -250,8 +226,10 @@ export class OB11Constructor { if (msg.chatType !== ChatType.group) { return; } + // log("group msg", msg); 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) { @@ -260,7 +238,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); @@ -280,7 +258,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表示全员禁言 @@ -288,14 +266,35 @@ 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); } } } 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) { + 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"); + } + } + } } } } @@ -328,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, 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 } } 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 8bbe09c..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 2e68519..93bafb5 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.16.0" \ No newline at end of file