From 33ae32459123243644e9865cd575f7b207ee2a12 Mon Sep 17 00:00:00 2001 From: xh321 Date: Sun, 6 Aug 2023 23:12:06 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=95=B0=E6=8D=AE=E5=BA=93?= =?UTF-8?q?=E6=94=AF=E6=8C=81=EF=BC=8C=E4=BD=BF=E5=BE=97=E9=87=8D=E5=90=AF?= =?UTF-8?q?QQ=E4=BB=8D=E8=83=BD=E9=83=A8=E5=88=86=E6=81=A2=E5=A4=8D?= =?UTF-8?q?=E6=92=A4=E5=9B=9E=E5=90=8E=E7=9A=84=E6=B6=88=E6=81=AF=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 17 ++++--- main.js | 136 +++++++++++++++++++++++++++++++++++++++++++++++--- manifest.json | 14 ++++-- package.json | 5 ++ renderer.js | 1 + 5 files changed, 155 insertions(+), 18 deletions(-) create mode 100644 package.json diff --git a/README.md b/README.md index 49772f1..5808e6d 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,15 @@ # LiteLoaderQQNT - anti-recall -LiteLoaderQQNT插件,用于简易的防撤回。 +LiteLoaderQQNT插件,用于比较完善的防撤回。 使用前需要安装[LiteLoaderQQNT](https://github.com/mo-jinran/LiteLoaderQQNT),并在QQNT新版上使用。 ## 使用方法 -clone或下载zip文件解压,保留文件夹结构(文件夹名称为`插件名`,内容为github上的内容),将文件夹移动至`LiteLoaderQQNT数据目录/plugins/`下面,重启QQNT即可。 +建议前往release中下载压缩包,保留文件夹结构(文件夹名称为`插件名`,内容为github上的内容),将文件夹移动至`LiteLoaderQQNT数据目录/plugins/`下面,重启QQNT即可。 -所有被撤回的消息会被带上红框,文字加上删除线。**重新进消息界面仍然可以看到。** 不过重启QQNT就会失效。 +直接克隆源码的话,需要手动进行`npm install`。 + +所有被撤回的消息会被带上红框,文字加上删除线。**重新进消息界面仍然可以看到。**当前版本重启QQ后**只能恢复文字和表情消息**,图片消息暂不可用,请等待版本更新。 @@ -15,6 +17,8 @@ clone或下载zip文件解压,保留文件夹结构(文件夹名称为`插 但是如果不是在眼前撤回的,而是撤回后你才点进聊天界面去看的,图片仍然会转圈一段时间(请耐心等待),猜测因为QQ会先检查图片是否存在,撤回后还没下载当然不存在,所以转圈;等插件下载好图片后,需要等待QQ重新检查,才能显示图片。 +图片如果撤回太快,反撤回后有可能会一直加载不出来,这个可能是图片已经从服务器上删除了,这种情况下,**无法恢复**。 + ## 原理介绍 @@ -25,18 +29,19 @@ clone或下载zip文件解压,保留文件夹结构(文件夹名称为`插 - 为避免太占内存,保存消息的上限默认为1000条。如有需要,可自行修改`main.js`中的`MAX_MSG_SAVED_LIMIT`常量。这个值的意义是,假设默认最多存1000条,那么总接到消息的1000条之前的消息若被撤回则无法恢复。**不过,若一条消息在撤回后你回去看了,并且反撤回生效(也就是只要你看到了这条消息出现红框和删除线),则这条消息会被额外储存到专门的“已撤回消息数组”中,这个数组没有容量上限,不会被本限制所约束。** - 为避免可能存在的并发问题,消息超过上述限制后,每次接到消息后默认会删除保存的消息列表中的前50条,直到低于上述上限。这个50条你也可以通过修改`main.js`中的`DELETE_MSG_COUNT_PER_TIME`常量来变更。建议修改为你平均每秒接到的消息数量。 +- 任何撤回的消息都会存入本地的数据库(位于LLQQNT的插件数据目录(`plugin_data`里),以便于重启后读取已撤回的消息记录。 -所以,目前反撤回**仅在进程生命周期有效**(重启QQNT失效,消息会被重新撤回)。 +所以,目前反撤回**重启QQ仍然生效**,但图片消息会恢复失败,请等待版本更新。 相关详细的原理和说明请参阅源码。 -**正在考虑用数据库储存,请耐心等待版本更新。** +**使用LevelDB进行数据储存**,还没加入启用和关闭储存的开关,请等待后续版本更新(**如果你不想让插件储存撤回后的消息,你可以暂且不更新版本**) ## 总结 在NTQQ打开期间无论怎样操作,**反撤回均能生效**,消息会暂时保存在内存里,不会落盘,所以没有消息泄露风险。(如果你能读插件内存里的消息,你为什么不直接去读QQ的消息列表呢🤣) -如果有安全的序列化储存方式,那么撤回持久化也不是什么问题。 +NTQQ重启后,仅文本和表情可恢复,图片会恢复失败。 ## 协议及免责 diff --git a/main.js b/main.js index 8cec599..e1c2a7a 100644 --- a/main.js +++ b/main.js @@ -3,13 +3,41 @@ const https = require("https"); const fs = require("fs"); const path = require("path"); -// const { Level } = require("level"); -// var db = null; +const { Level } = require("level"); +var db = null; -function onLoad(plugin) { - // db = new Level(path.join(plugin.path.data, "qq-recalled-db"), { - // valueEncoding: "json" - // }); +async function onLoad(plugin) { + db = new Level(path.join(plugin.path.data, "qq-recalled-db"), { + valueEncoding: "json" + }); + + output("Loading recalled msgs from db..."); + var counter = 0; + for await (const value of db.values()) { + counter++; + recalledMsg.push(value); + if (value.msg == null) continue; + for (item of value.msg.elements) { + if (item.picElement != null) { + item.picElement = null; + item.elementType = 1; + item.textElement = { + content: "[暂不支持图片消息恢复,请等待反撤回版本更新]", + atType: 0, + atUid: "0", + atTinyId: "0", + atNtUid: "", + subElementType: 0, + atChannelId: "0", + atRoleId: "0", + atRoleColor: 0, + atRoleName: "", + needNotify: 0 + }; + } + } + } + output(`Loaded ${counter} msgs.`); } var msgFlow = []; @@ -81,6 +109,31 @@ async function downloadPic(msgList) { } } +function insertDb(msg) { + if (db != null) { + db.get(msg.id, (error, value) => { + if (error.status == 404) { + db.put(msg.id, msg, (err) => { + if (err) throw err; + }); + } else { + if (error) throw error; + } + }); + } +} + +async function getMsgById(id) { + if (db != null) { + try { + return await db.get(id); + } catch { + return null; + } + } + return null; +} + function onBrowserWindowCreated(window) { window.webContents.on("did-stop-loading", () => { //只针对主界面和独立聊天界面生效 @@ -88,6 +141,25 @@ function onBrowserWindowCreated(window) { window.webContents.getURL().indexOf("#/main/message") != -1 || window.webContents.getURL().indexOf("#/chat/") != -1 ) { + // const proxyEvents = new Proxy( + // window.webContents._events["-ipc-message"], + // { + // // 拦截函数调用 + // apply(target, thisArg, argumentsList) { + // if ( + // argumentsList[3][0]["eventName"] && + // !argumentsList[3][0]["eventName"].includes( + // "ns-Logger" + // ) + // ) { + // output(JSON.stringify(argumentsList)); + // } + // return target.apply(thisArg, argumentsList); + // } + // } + // ); + // window.webContents._events["-ipc-message"] = proxyEvents; + //补充知识: //撤回的原理是,你先发了一条消息,这条消息有一个msgId,然后又撤回了他,那腾讯就会发一条一样msgId的撤回消息包来替换,这样你以后拉取的话,这个msgId只会对应一条撤回提示了; //本插件的原理是,先在内存中临时储存所有消息(1000条上限),然后如果有撤回发生,则将撤回的提示替换为之前保存的消息。 @@ -99,6 +171,7 @@ function onBrowserWindowCreated(window) { //var myUid = ""; const patched_send = function (channel, ...args) { + //output(channel, JSON.stringify(args)); // if (db != null) { // db.put("a", { x: 123 }, function (err) { // if (err) throw err; @@ -160,13 +233,48 @@ function onBrowserWindowCreated(window) { (i) => i.id == currMsgId ); + //var dbMsg = getMsgById(currMsgId); + //优先从已保存的撤回的消息中获取 if (olderMsgFromRecalledMsg != null) { + // original_send.call( + // window.webContents, + // channel, + // { + // type: "request", + // eventName: "ns-ntApi-2" + // }, + // [ + // { + // cmdName: + // "nodeIKernelMsgListener/onRecvMsg", + // cmdType: "event", + // payload: { + // msgList: [ + // olderMsgFromRecalledMsg.msg + // ] + // } + // } + // ] + // ); + downloadPic(olderMsgFromRecalledMsg.msg); + console.log( + JSON.stringify( + olderMsgFromRecalledMsg.msg + ) + ); + args[1].msgList[i] = olderMsgFromRecalledMsg.msg; + console.log( + JSON.stringify( + olderMsgFromRecalledMsg.msg + ) + ); + output( "Detected recall, intercepted and recovered from old msg" ); @@ -186,6 +294,20 @@ function onBrowserWindowCreated(window) { "Detected recall, intercepted and recovered from msgFlow" ); } + // else if (dbMsg != null) { + // args[1].msgList[i] = dbMsg.msg; + + // //没专门存过这条消息到专门的反撤回数组中,就存一下 + // if (olderMsgFromRecalledMsg == null) { + // recalledMsg.push(dbMsg); + // } + + // downloadPic(dbMsg.msg); + + // output( + // "Detected recall, intercepted and recovered from dbMsg" + // ); + // } }); window.webContents.send( @@ -255,6 +377,7 @@ function onBrowserWindowCreated(window) { olderMsgFromRecalledMsg == null ) { recalledMsg.push(olderMsg); + insertDb(olderMsg); } downloadPic(olderMsg?.msg); @@ -281,7 +404,6 @@ function onBrowserWindowCreated(window) { for (msg of msgList) { var msgId = msg.msgId; - msgFlow.push({ id: msgId, msg: msg }); if (msgFlow.length > MAX_MSG_SAVED_LIMIT) { msgFlow.splice( diff --git a/manifest.json b/manifest.json index 06148fc..0118f5a 100644 --- a/manifest.json +++ b/manifest.json @@ -4,15 +4,19 @@ "name": "防撤回", "slug": "anti_recall", "description": "防止QQNT撤回消息", - "version": "0.2.6", + "version": "0.2.7", "thumbnail": "./icon.png", "author": { - "name": "XiaoHe321", - "link": "https://github.com/xh321" - }, + "name": "XiaoHe321", + "link": "https://github.com/xh321" + }, "repository": { "repo": "xh321/LiteLoaderQQNT-Anti-Recall", - "branch": "master" + "branch": "master", + "use_release": { + "tag": "latest", + "name": "qq-anti-recall.zip" + } }, "platform": ["win32", "darwin", "linux"], "injects": { diff --git a/package.json b/package.json new file mode 100644 index 0000000..3055feb --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "level": "^8.0.0" + } +} diff --git a/renderer.js b/renderer.js index 36a3450..1569d24 100644 --- a/renderer.js +++ b/renderer.js @@ -25,6 +25,7 @@ export function onLoad() { //消息列表更新回调 anti_recall.recallTipList((event, msgIdList) => { recalledMsgList = msgIdList; + render(); }); //监控消息列表,如果有撤回则渲染