Merge branch 'dev'

# Conflicts:
#	src/main/main.ts
#	src/renderer/index.ts
This commit is contained in:
linyuchen
2024-03-17 15:41:32 +08:00
47 changed files with 1648 additions and 1118 deletions

View File

@@ -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: {}
}
}

View File

@@ -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": [
{

View File

@@ -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<Group | undefined> {
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)
}
}

342
src/common/utils/index.ts Normal file
View File

@@ -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<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 定义一个异步函数来检查文件是否存在
export function checkFileReceived(path: string, timeout: number = 3000): Promise<void> {
return new Promise((resolve, reject) => {
const startTime = Date.now();
function check() {
if (fs.existsSync(path)) {
resolve();
} else if (Date.now() - startTime > timeout) {
reject(new Error(`文件不存在: ${path}`));
} else {
setTimeout(check, 100);
}
}
check();
});
}
export async function 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<boolean> {
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<string>((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<string> {
return new Promise((resolve, reject) => {
// 创建一个流式读取器
const stream = fs.createReadStream(filePath);
const hash = crypto.createHash('md5');
stream.on('data', (data: Buffer) => {
// 当读取到数据时,更新哈希对象的状态
hash.update(data);
});
stream.on('end', () => {
// 文件读取完成,计算哈希
const md5 = hash.digest('hex');
resolve(md5);
});
stream.on('error', (err: Error) => {
// 处理可能的读取错误
reject(err);
});
});
}

View File

@@ -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
}

10
src/common/utils/qqpkg.ts Normal file
View File

@@ -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"))

View File

@@ -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<RawMessage> }>(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<RawMessage> }>([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<RawMessage> }>(ReceiveCmd.UPDATE_MSG, async (payload) => {
registerReceiveHook<{ msgList: Array<RawMessage> }>([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
}

217
src/ntqqapi/api/file.ts Normal file
View File

@@ -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<string>({
className: NTQQApiClass.FS_API,
methodName: NTQQApiMethod.FILE_MD5,
args: [filePath]
})
}
static async copyFile(filePath: string, destPath: string) {
return await callNTQQApi<string>({
className: NTQQApiClass.FS_API,
methodName: NTQQApiMethod.FILE_COPY,
args: [{
fromPath: filePath,
toPath: destPath
}]
})
}
static async getFileSize(filePath: string) {
return await callNTQQApi<number>({
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<string>({
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<GeneralCallResult>({
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<string> = ['tmp', 'hotUpdate']) {
return callNTQQApi<any>({ // TODO: 目前还不知道真正的返回值是什么
methodName: NTQQApiMethod.CACHE_CLEAR,
args: [{
keys: cacheKeys
}, null]
});
}
static addCacheScannedPaths(pathMap: object = {}) {
return callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.CACHE_ADD_SCANNED_PATH,
args: [{
pathMap: {...pathMap},
}, null]
});
}
static scanCache() {
callNTQQApi<GeneralCallResult>({
methodName: ReceiveCmdS.CACHE_SCAN_FINISH,
classNameIsRegister: true,
}).then();
return callNTQQApi<CacheScanResult>({
methodName: NTQQApiMethod.CACHE_SCAN,
args: [null, null],
timeoutSecond: 300,
});
}
static getHotUpdateCachePath() {
return callNTQQApi<string>({
className: NTQQApiClass.HOTUPDATE_API,
methodName: NTQQApiMethod.CACHE_PATH_HOT_UPDATE
});
}
static getDesktopTmpPath() {
return callNTQQApi<string>({
className: NTQQApiClass.BUSINESS_API,
methodName: NTQQApiMethod.CACHE_PATH_DESKTOP_TEMP
});
}
static getChatCacheList(type: ChatType, pageSize: number = 1000, pageIndex: number = 0) {
return new Promise<ChatCacheList>((res, rej) => {
callNTQQApi<ChatCacheList>({
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<CacheFileList>({
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<GeneralCallResult>({
methodName: NTQQApiMethod.CACHE_CHAT_CLEAR,
args: [{
chats,
fileKeys
}, null]
});
}
}

61
src/ntqqapi/api/friend.ts Normal file
View File

@@ -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<GeneralCallResult>({
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<GeneralCallResult>({
methodName: NTQQApiMethod.HANDLE_FRIEND_REQUEST,
args: [
{
"approvalInfo": {
"friendUid": request.friendUid,
"reqTime": request.reqTime,
accept
}
}
]
})
delete friendRequests[sourceId];
return result;
}
}

223
src/ntqqapi/api/group.ts Normal file
View File

@@ -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<GroupMember[]> {
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<GeneralCallResult>({
methodName: ReceiveCmdS.GROUP_NOTIFY,
classNameIsRegister: true,
}).then()
return await callNTQQApi<GroupNotifies>({
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<GroupNotifies>({
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<GeneralCallResult>({
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<GeneralCallResult>({
methodName: NTQQApiMethod.QUIT_GROUP,
args: [
{"groupCode": groupQQ},
null
]
})
}
static async kickMember(groupQQ: string, kickUids: string[], refuseForever: boolean = false, kickReason: string = '') {
return await callNTQQApi<GeneralCallResult>(
{
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<GeneralCallResult>(
{
methodName: NTQQApiMethod.MUTE_MEMBER,
args: [
{
groupCode: groupQQ,
memList,
}
]
}
)
}
static async banGroup(groupQQ: string, shutUp: boolean) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.MUTE_GROUP,
args: [
{
groupCode: groupQQ,
shutUp
}, null
]
})
}
static async setMemberCard(groupQQ: string, memberUid: string, cardName: string) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.SET_MEMBER_CARD,
args: [
{
groupCode: groupQQ,
uid: memberUid,
cardName
}, null
]
})
}
static async setMemberRole(groupQQ: string, memberUid: string, role: GroupMemberRole) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.SET_MEMBER_ROLE,
args: [
{
groupCode: groupQQ,
uid: memberUid,
role
}, null
]
})
}
static async setGroupName(groupQQ: string, groupName: string) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.SET_GROUP_NAME,
args: [
{
groupCode: groupQQ,
groupName
}, null
]
})
}
// 头衔不可用
static async setGroupTitle(groupQQ: string, uid: string, title: string) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.SET_GROUP_TITLE,
args: [
{
groupCode: groupQQ,
uid,
title
}, null
]
})
}
static publishGroupBulletin(groupQQ: string, title: string, content: string) {
}
}

187
src/ntqqapi/api/msg.ts Normal file
View File

@@ -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<string, ((sendSuccessMsg: RawMessage) => 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<GeneralCallResult>({
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<RawMessage> => {
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<GeneralCallResult>({
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<RawMessage>((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<GeneralCallResult>({
methodName: NTQQApiMethod.MULTI_FORWARD_MSG,
args: apiArgs
}).then(result => {
log("转发消息结果:", result, apiArgs)
if (result.result !== 0) {
complete = true;
reject("转发消息失败," + JSON.stringify(result));
}
})
})
}
}

57
src/ntqqapi/api/user.ts Normal file
View File

@@ -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<GeneralCallResult>({
methodName: NTQQApiMethod.SET_QQ_AVATAR,
args: [{
path:filePath
}, null],
timeoutSecond: 10 // 10秒不一定够
});
}
static async getSelfInfo() {
return await callNTQQApi<SelfInfo>({
className: NTQQApiClass.GLOBAL_DATA,
methodName: NTQQApiMethod.SELF_INFO, timeoutSecond: 2
})
}
static async getUserInfo(uid: string) {
const result = await callNTQQApi<{ profiles: Map<string, User> }>({
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
}
}

View File

@@ -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<SendPicElement> {
const {md5, fileName, path, fileSize} = await NTQQApi.uploadFile(picPath, ElementType.PIC);
static async pic(picPath: string, summary: string = ""): Promise<SendPicElement> {
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<SendFileElement> {
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<SendVideoElement> {
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<SendPttElement> {
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";
}

28
src/ntqqapi/external/ccpoke/index.ts vendored Normal file
View File

@@ -0,0 +1,28 @@
import {log} from "../../../common/utils";
let pokeEngine: any = null
type PokeHandler = (id: string, isGroup: boolean)=>void
let pokeRecords: Record<string, number> = {}
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);
})
}

Binary file not shown.

View File

@@ -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<string, (apiReturn: any) => 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<PayloadType = unknown> extends Array<any> {
0: {
"type": "request",
@@ -46,7 +51,7 @@ interface NTQQApiReturnData<PayloadType = unknown> extends Array<any> {
}
let receiveHooks: Array<{
method: ReceiveCmd,
method: ReceiveCmd[],
hookFunc: ((payload: any) => void | Promise<void>)
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<PayloadType>(method: ReceiveCmd, hookFunc: (payload: PayloadType) => void): string {
export function registerReceiveHook<PayloadType>(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<string>(); // 建立索引降低时间复杂度
@@ -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<RawMessage> }>(ReceiveCmd.NEW_MSG, (payload) => {
registerReceiveHook<{ msgList: Array<RawMessage> }>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], (payload) => {
const {autoDeleteFile} = getConfigUtil().getConfig();
if (!autoDeleteFile) {
return
@@ -241,7 +273,7 @@ registerReceiveHook<{ msgList: Array<RawMessage> }>(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<RawMessage> }>(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
})

View File

@@ -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<ReturnType>(params: NTQQApiParams) {
export function callNTQQApi<ReturnType>(params: NTQQApiParams) {
let {
className, methodName, channel, args,
cbCmd, timeoutSecond: timeout,
@@ -199,574 +161,13 @@ function callNTQQApi<ReturnType>(params: NTQQApiParams) {
}
export let sendMessagePool: Record<string, ((sendSuccessMsg: RawMessage) => 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<GeneralCallResult>({
methodName: NTQQApiMethod.SET_HEADER,
args: [path]
})
}
static async likeFriend(uid: string, count = 1) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.LIKE_FRIEND,
args: [{
doLikeUserInfo: {
friendUid: uid,
sourceId: 71,
doLikeCount: count,
doLikeTollCount: 0
}
}, null]
})
}
static async getSelfInfo() {
return await callNTQQApi<SelfInfo>({
className: NTQQApiClass.GLOBAL_DATA,
methodName: NTQQApiMethod.SELF_INFO, timeoutSecond: 2
})
}
static async getUserInfo(uid: string) {
const result = await callNTQQApi<{ profiles: Map<string, User> }>({
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<GroupMember[]> {
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<string>({
className: NTQQApiClass.FS_API,
methodName: NTQQApiMethod.FILE_MD5,
args: [filePath]
})
}
static async copyFile(filePath: string, destPath: string) {
return await callNTQQApi<string>({
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<number>({
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<string>({
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<RawMessage> => {
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<GeneralCallResult>({
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<RawMessage>((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<GeneralCallResult>({
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<GeneralCallResult>({
methodName: ReceiveCmd.GROUP_NOTIFY,
classNameIsRegister: true,
}).then()
return await callNTQQApi<GroupNotifies>({
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<GroupNotifies>({
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<GeneralCallResult>({
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<GeneralCallResult>({
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<GeneralCallResult>({
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<GeneralCallResult>(
{
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<GeneralCallResult>(
{
methodName: NTQQApiMethod.MUTE_MEMBER,
args: [
{
groupCode: groupQQ,
memList,
}
]
}
)
}
static async banGroup(groupQQ: string, shutUp: boolean) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.MUTE_GROUP,
args: [
{
groupCode: groupQQ,
shutUp
}, null
]
})
}
static async setMemberCard(groupQQ: string, memberUid: string, cardName: string) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.SET_MEMBER_CARD,
args: [
{
groupCode: groupQQ,
uid: memberUid,
cardName
}, null
]
})
}
static async setMemberRole(groupQQ: string, memberUid: string, role: GroupMemberRole) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.SET_MEMBER_ROLE,
args: [
{
groupCode: groupQQ,
uid: memberUid,
role
}, null
]
})
}
static async setGroupName(groupQQ: string, groupName: string) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.SET_GROUP_NAME,
args: [
{
groupCode: groupQQ,
groupName
}, null
]
})
}
static async call(className: NTQQApiClass, cmdName: string, args: any[],) {
return await callNTQQApi<GeneralCallResult>({
className,
@@ -777,133 +178,4 @@ export class NTQQApi {
})
}
static async setGroupTitle(groupQQ: string, uid: string, title: string) {
return await callNTQQApi<GeneralCallResult>({
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<GeneralCallResult>({
methodName: NTQQApiMethod.CACHE_SET_SILENCE,
args: [{
isSilent
}, null]
});
}
static addCacheScannedPaths(pathMap: object = {}) {
return callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.CACHE_ADD_SCANNED_PATH,
args: [{
pathMap: {...pathMap},
}, null]
});
}
static scanCache() {
callNTQQApi<GeneralCallResult>({
methodName: ReceiveCmd.CACHE_SCAN_FINISH,
classNameIsRegister: true,
}).then();
return callNTQQApi<CacheScanResult>({
methodName: NTQQApiMethod.CACHE_SCAN,
args: [null, null],
timeoutSecond: 300,
});
}
static getHotUpdateCachePath() {
return callNTQQApi<string>({
className: NTQQApiClass.HOTUPDATE_API,
methodName: NTQQApiMethod.CACHE_PATH_HOT_UPDATE
});
}
static getDesktopTmpPath() {
return callNTQQApi<string>({
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<string> = ['tmp', 'hotUpdate']) {
return callNTQQApi<any>({ // TODO: 目前还不知道真正的返回值是什么
methodName: NTQQApiMethod.CACHE_CLEAR,
args: [{
keys: cacheKeys
}, null]
});
}
static getChatCacheList(type: ChatType, pageSize: number = 1000, pageIndex: number = 0) {
return new Promise<ChatCacheList>((res, rej) => {
callNTQQApi<ChatCacheList>({
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<CacheFileList>({
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<GeneralCallResult>({
methodName: NTQQApiMethod.CACHE_CHAT_CLEAR,
args: [{
chats,
fileKeys
}, null]
});
}
static async setQQAvatar(filePath: string) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.SET_QQ_AVATAR,
args: [{
path:filePath
}, null],
timeoutSecond: 10 // 10秒不一定够
});
}
}

View File

@@ -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,
}

View File

@@ -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
}

View File

@@ -0,0 +1,7 @@
export * from './user';
export * from './group';
export * from './msg';
export * from './notify';
export * from './cache';

View File

@@ -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,
}
}

View File

@@ -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[]
}
}

28
src/ntqqapi/types/user.ts Normal file
View File

@@ -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 {}

View File

@@ -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<void, void> {
actionName = ActionName.CleanCache
@@ -19,21 +19,21 @@ export default class CleanCache extends BaseAction<void, void> {
// 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<void, void> {
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<Array<ChatCacheListItemBasic>>((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 => {

View File

@@ -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<Payload, void> {
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])

View File

@@ -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<PayloadType, OB11GroupMember> {
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}不存在`)

View File

@@ -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<PayloadType, OB11GroupMember[]> {
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 {

View File

@@ -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<Payload, null> {
} 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
}

View File

@@ -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<OB11PostSendMsg, ReturnDataType> {
}
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<OB11PostSendMsg, ReturnDataType> {
// 开发转发
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<OB11PostSendMsg, ReturnDataType> {
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<OB11PostSendMsg, ReturnDataType> {
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, () => {

View File

@@ -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<Payload, null> {
actionName = ActionName.SetFriendAddRequest;
protected async _handle(payload: Payload): Promise<null> {
await NTQQApi.handleFriendRequest(parseInt(payload.flag), payload.approve)
await NTQQFriendApi.handleFriendRequest(parseInt(payload.flag), payload.approve)
return null;
}
}

View File

@@ -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<Payload, null> {
protected async _handle(payload: Payload): Promise<null> {
const seq = payload.flag.toString();
await NTQQApi.handleGroupRequest(seq,
await NTQQGroupApi.handleGroupRequest(seq,
payload.approve ? GroupRequestOperateTypes.approve : GroupRequestOperateTypes.reject,
payload.reason
)

View File

@@ -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<Payload, null> {
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
}
}

View File

@@ -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<Payload, null> {
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
}

View File

@@ -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<Payload, null> {
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
}
}

View File

@@ -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<Payload, null> {
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
}
}

View File

@@ -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<Payload, any> {
protected async _handle(payload: Payload): Promise<any> {
try {
await NTQQApi.quitGroup(payload.group_id.toString())
await NTQQGroupApi.quitGroup(payload.group_id.toString())
} catch (e) {
log("退群失败", e)
throw e

View File

@@ -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<Payload, null> {
protected async _handle(payload: Payload): Promise<null> {
await NTQQApi.setGroupName(payload.group_id.toString(), payload.group_name)
await NTQQGroupApi.setGroupName(payload.group_id.toString(), payload.group_name)
return null
}
}

View File

@@ -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<Payload, null> {
protected async _handle(payload: Payload): Promise<null> {
await NTQQApi.banGroup(payload.group_id.toString(), !!payload.enable)
await NTQQGroupApi.banGroup(payload.group_id.toString(), !!payload.enable)
return null
}
}

View File

@@ -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<Payload, null> {
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
}
}

View File

@@ -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<null, OB11GroupReques
actionName = ActionName.GetGroupIgnoreAddRequest
protected async _handle(payload: null): Promise<OB11GroupRequestNotify[]> {
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),

View File

@@ -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<Payload, null> {
}
if (path) {
await checkFileReceived(path, 5000); // 文件不存在QQ会崩溃需要提前判断
const ret = await NTQQApi.setQQAvatar(path)
const ret = await NTQQUserApi.setQQAvatar(path)
if (!isLocal){
fs.unlink(path, () => {})
}

View File

@@ -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,

View File

@@ -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
}
}

View File

@@ -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;
}
}

View File

@@ -121,6 +121,9 @@ interface OB11MessageFileBase {
export interface OB11MessageImage extends OB11MessageFileBase {
type: OB11MessageDataType.image
data: OB11MessageFileBase['data'] & {
summary ? : string; // 图片摘要
}
}
export interface OB11MessageRecord extends OB11MessageFileBase {

View File

@@ -90,8 +90,8 @@ async function onSettingWindowCreated(view: Element) {
], 'ob11.messagePostFormat', config.ob11.messagePostFormat),
),
SettingItem(
'ffmpeg 路径发送语音、视频需要同时保证ffprobe和ffmpeg在一起', `配置可参考 <a href="javascript:LiteLoader.api.openExternal(\'https://llonebot.github.io/zh-CN/guide/ffmpeg\');">官方文档</a> <span id="config-ffmpeg-path-text"> 路径:${!isEmpty(config.ffmpeg) ? config.ffmpeg : '未指定'}</span>`,
SettingButton('选择', 'config-ffmpeg-select'),
'ffmpeg 路径发送语音、视频需要同时保证ffprobe和ffmpeg在一起', ` <a href="javascript:LiteLoader.api.openExternal(\'https://llonebot.github.io/zh-CN/guide/ffmpeg\');">下载地址</a> <span id="config-ffmpeg-path-text">, 路径:${!isEmpty(config.ffmpeg) ? config.ffmpeg : '未指定'}</span>`,
SettingButton('选择ffmpeg', 'config-ffmpeg-select'),
),
SettingItem(
'', null,

View File

@@ -1 +1 @@
export const version = "3.15.1"
export const version = "3.16.0"