Skip to content

Commit

Permalink
增加数据库支持,使得重启QQ仍能部分恢复撤回后的消息。
Browse files Browse the repository at this point in the history
  • Loading branch information
xh321 committed Aug 6, 2023
1 parent e08ff4e commit 33ae324
Show file tree
Hide file tree
Showing 5 changed files with 155 additions and 18 deletions.
17 changes: 11 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
# 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后**只能恢复文字和表情消息**,图片消息暂不可用,请等待版本更新。



现在,即使图片在撤回之前未打开过,撤回后也能继续查看了,并且不止缩略图,点开看也是可以的!

但是如果不是在眼前撤回的,而是撤回后你才点进聊天界面去看的,图片仍然会转圈一段时间(请耐心等待),猜测因为QQ会先检查图片是否存在,撤回后还没下载当然不存在,所以转圈;等插件下载好图片后,需要等待QQ重新检查,才能显示图片。

图片如果撤回太快,反撤回后有可能会一直加载不出来,这个可能是图片已经从服务器上删除了,这种情况下,**无法恢复**



## 原理介绍
Expand All @@ -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重启后,仅文本和表情可恢复,图片会恢复失败

## 协议及免责

Expand Down
136 changes: 129 additions & 7 deletions main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [];
Expand Down Expand Up @@ -81,13 +109,57 @@ 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", () => {
//只针对主界面和独立聊天界面生效
if (
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条上限),然后如果有撤回发生,则将撤回的提示替换为之前保存的消息。
Expand All @@ -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;
Expand Down Expand Up @@ -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"
);
Expand All @@ -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(
Expand Down Expand Up @@ -255,6 +377,7 @@ function onBrowserWindowCreated(window) {
olderMsgFromRecalledMsg == null
) {
recalledMsg.push(olderMsg);
insertDb(olderMsg);
}

downloadPic(olderMsg?.msg);
Expand All @@ -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(
Expand Down
14 changes: 9 additions & 5 deletions manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"dependencies": {
"level": "^8.0.0"
}
}
1 change: 1 addition & 0 deletions renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export function onLoad() {
//消息列表更新回调
anti_recall.recallTipList((event, msgIdList) => {
recalledMsgList = msgIdList;
render();
});

//监控消息列表,如果有撤回则渲染
Expand Down

0 comments on commit 33ae324

Please sign in to comment.