加入配置界面,可动态调整撤回样式以及是否启用数据库

This commit is contained in:
xh321
2023-08-07 23:54:46 +08:00
parent 33ae324591
commit a67d590443
5 changed files with 621 additions and 102 deletions
+20 -14
View File
@@ -9,7 +9,25 @@ LiteLoaderQQNT插件,用于比较完善的防撤回。
直接克隆源码的话,需要手动进行`npm install`。
所有被撤回的消息会被带上红框,文字加上删除线。**重新进消息界面仍然可以看到。**当前版本重启QQ后**只能恢复文字和表情消息**,图片消息暂不可用,请等待版本更新。
## 太长不看版
在NTQQ打开期间无论怎样操作,**反撤回均能生效**。
若开启数据库储存(**默认关闭**,请前往配置界面手动开启),NTQQ重启后可恢复已撤回的消息,但仅文本和表情可恢复,图片会恢复失败(请等待版本更新)。
若不满意撤回的颜色、阴影和提示,也请前往配置界面手动调整。
若反撤回消息太多,累积在数据库里可能造成加载卡顿,建议隔一段时间通过插件配置界面清理一次已储存的消息。
## 详细说明
所有被撤回的消息会被带上红框,下方会出现“已撤回”的小提示(均可通过配置关闭)。**重新进消息界面已撤回的消息仍然可以正常显示。**
**使用LevelDB进行数据持久化储存**,默认**不**启用,需要手动去设置里开启。如果不开启,重启QQ消息会丢失。若开启,当前版本重启QQ后**只能恢复文字和表情消息**,图片消息暂不可恢复,请等待版本更新。
如果你需要调整撤回样式,请前往插件的设置界面,手动修改调整。
不建议手动修改配置文件,修改后不会实时生效(需要重启QQ),建议用配置界面修改。
@@ -19,8 +37,6 @@ LiteLoaderQQNT插件,用于比较完善的防撤回。
图片如果撤回太快,反撤回后有可能会一直加载不出来,这个可能是图片已经从服务器上删除了,这种情况下,**无法恢复**。
## 原理介绍
**之前版本(过时):** 为什么在简介里说是简易的呢?因为这个防撤回原理是通过拦截撤回消息的IPC,这样消息就不会被删除。可是,若重新打开消息界面,NTQQ似乎会重新向逻辑层拉取一次消息(旧版QQ的防撤回直接从通信上就拦截了撤回包,所以没有这个问题;而本插件仅拦截到了渲染层的IPC,逻辑层还是一个黑盒),所以导致消息仍然被撤回了。
@@ -29,20 +45,10 @@ LiteLoaderQQNT插件,用于比较完善的防撤回。
- 为避免太占内存,保存消息的上限默认为1000条。如有需要,可自行修改`main.js`中的`MAX_MSG_SAVED_LIMIT`常量。这个值的意义是,假设默认最多存1000条,那么总接到消息的1000条之前的消息若被撤回则无法恢复。**不过,若一条消息在撤回后你回去看了,并且反撤回生效(也就是只要你看到了这条消息出现红框和删除线),则这条消息会被额外储存到专门的“已撤回消息数组”中,这个数组没有容量上限,不会被本限制所约束。**
- 为避免可能存在的并发问题,消息超过上述限制后,每次接到消息后默认会删除保存的消息列表中的前50条,直到低于上述上限。这个50条你也可以通过修改`main.js`中的`DELETE_MSG_COUNT_PER_TIME`常量来变更。建议修改为你平均每秒接到的消息数量。
- 任何撤回的消息都会存入本地的数据库(位于LLQQNT的插件数据目录(`plugin_data`里),以便于重启后读取已撤回的消息记录。
所以,目前反撤回**重启QQ仍然生效**,但图片消息会恢复失败,请等待版本更新。
- 若开启数据库储存功能,则任何撤回的消息都会存入本地的数据库(位于LLQQNT的插件数据目录(`plugin_data`里),以便于重启后读取已撤回的消息记录。
相关详细的原理和说明请参阅源码。
**使用LevelDB进行数据储存**,还没加入启用和关闭储存的开关,请等待后续版本更新(**如果你不想让插件储存撤回后的消息,你可以暂且不更新版本**)
## 总结
在NTQQ打开期间无论怎样操作,**反撤回均能生效**,消息会暂时保存在内存里,不会落盘,所以没有消息泄露风险。(如果你能读插件内存里的消息,你为什么不直接去读QQ的消息列表呢🤣)
NTQQ重启后,仅文本和表情可恢复,图片会恢复失败。
## 协议及免责
MIT | 禁止用于任何非法用途,插件开发属学习与研究目的,仅自用,未提供给任何第三方使用。任何不当使用导致的任何侵权问题责任自负。
+139 -38
View File
@@ -2,42 +2,144 @@ const http = require("http");
const https = require("https");
const fs = require("fs");
const path = require("path");
const { ipcMain, dialog } = require("electron");
var configFilePath = "";
const { Level } = require("level");
var db = null;
var sampleConfig = {
mainColor: "#ff6d6d",
saveDb: false,
enableShadow: true,
enableTip: true
};
var nowConfig = {};
function initConfig() {
fs.writeFileSync(
configFilePath,
JSON.stringify(sampleConfig, null, 2),
"utf-8"
);
}
function loadConfig() {
if (!fs.existsSync(configFilePath)) {
initConfig();
return sampleConfig;
} else {
return JSON.parse(fs.readFileSync(configFilePath, "utf-8"));
}
}
async function onLoad(plugin) {
configFilePath = path.join(plugin.path.data, "config.json");
nowConfig = loadConfig();
if (nowConfig.mainColor == null) {
nowConfig.mainColor = "#ff6d6d";
}
if (nowConfig.enableShadow == null) {
nowConfig.enableShadow = true;
}
if (nowConfig.enableTip == null) {
nowConfig.enableTip = true;
}
fs.writeFileSync(
configFilePath,
JSON.stringify(nowConfig, null, 2),
"utf-8"
);
ipcMain.handle(
"LiteLoader.anti_recall.getNowConfig",
async (event, message) => {
return nowConfig;
}
);
ipcMain.handle(
"LiteLoader.anti_recall.saveConfig",
async (event, config) => {
nowConfig = config;
sendChatWindowsMessage(
"LiteLoader.anti_recall.mainWindow.repatchCss"
);
fs.writeFileSync(
configFilePath,
JSON.stringify(config, null, 2),
"utf-8"
);
}
);
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
};
ipcMain.handle("LiteLoader.anti_recall.clearDb", async (event, message) => {
dialog
.showMessageBox({
type: "warning",
title: "警告",
message: "清空所有已储存的撤回消息后不可恢复,是否确认清空?",
buttons: ["确定", "取消"],
cancelId: 1
})
.then(async (idx) => {
//第一个按钮
if (idx.response == 0) {
await db.clear();
dialog.showMessageBox({
type: "info",
title: "提示",
message:
"清空完毕,之前保存的所有已撤回消息均被删除,重启QQ后就能看见效果。",
buttons: ["确定"]
});
}
});
});
(async () => {
if (nowConfig.saveDb) {
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.`);
} else {
output("Db saving is disabled, continue.");
}
}
output(`Loaded ${counter} msgs.`);
})().catch((e) => {
output("Error while loading recalled msgs from db: " + e.toString());
});
}
var msgFlow = [];
@@ -134,6 +236,14 @@ async function getMsgById(id) {
return null;
}
function sendChatWindowsMessage(message) {
for (var window of mainWindowObjs) {
if (window.isDestroyed()) continue;
window.webContents.send(message);
}
}
var mainWindowObjs = [];
function onBrowserWindowCreated(window) {
window.webContents.on("did-stop-loading", () => {
//只针对主界面和独立聊天界面生效
@@ -141,6 +251,7 @@ function onBrowserWindowCreated(window) {
window.webContents.getURL().indexOf("#/main/message") != -1 ||
window.webContents.getURL().indexOf("#/chat/") != -1
) {
mainWindowObjs.push(window);
// const proxyEvents = new Proxy(
// window.webContents._events["-ipc-message"],
// {
@@ -260,21 +371,9 @@ function onBrowserWindowCreated(window) {
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"
);
@@ -377,7 +476,9 @@ function onBrowserWindowCreated(window) {
olderMsgFromRecalledMsg == null
) {
recalledMsg.push(olderMsg);
insertDb(olderMsg);
if (nowConfig.saveDb) {
insertDb(olderMsg);
}
}
downloadPic(olderMsg?.msg);
+1 -1
View File
@@ -4,7 +4,7 @@
"name": "防撤回",
"slug": "anti_recall",
"description": "防止QQNT撤回消息",
"version": "0.2.7",
"version": "0.2.9",
"thumbnail": "./icon.png",
"author": {
"name": "XiaoHe321",
+10
View File
@@ -1,6 +1,16 @@
const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("anti_recall", {
clearDb: () => ipcRenderer.invoke("LiteLoader.anti_recall.clearDb"),
getNowConfig: () =>
ipcRenderer.invoke("LiteLoader.anti_recall.getNowConfig"),
saveConfig: (config) =>
ipcRenderer.invoke("LiteLoader.anti_recall.saveConfig", config),
repatchCss: (callback) =>
ipcRenderer.on(
"LiteLoader.anti_recall.mainWindow.repatchCss",
callback
),
recallTip: (callback) =>
ipcRenderer.on("LiteLoader.anti_recall.mainWindow.recallTip", callback),
recallTipList: (callback) =>
+451 -49
View File
@@ -1,7 +1,423 @@
var recalledMsgList = [];
export function onLoad() {
export async function onConfigView(view) {
var nowConfig = await window.anti_recall.getNowConfig();
const new_navbar_item = `
<body>
<div class="config_view">
<section class="path">
<h1>主配置</h1>
<div class="wrap">
<div class="list">
<div class="vertical-list-item top-box">
<h2>操作</h2>
<div>
<button id="clearDb" class="q-button q-button--small q-button--secondary">清空已储存的撤回消息</button>
</div>
</div>
<hr class="horizontal-dividing-line" />
<div class="vertical-list-item">
<div style="width:90%;" >
<h2>是否将撤回消息存入数据库</h2>
<span class="secondary-text">数据库不加密,若开启风险自负;若不开启,重启QQ后撤回消息会丢失;开启选项后,反撤回消息才会开始保存;若之前开过,现在关闭,储存的消息不会被删除,需要你手动清理。</span>
</div>
<div id="switchSaveDb" class="q-switch">
<span class="q-switch__handle"></span>
</div>
</div>
</div>
</div>
</section>
<section class="path">
<h1>样式配置</h1>
<div class="wrap">
<div class="list">
<div class="vertical-list-item">
<div>
<h2>撤回主题色</h2></h2>
<span class="secondary-text">将会同时影响阴影和“已撤回”提示的颜色</span>
</div>
<div>
<input type="color" value="#ff0000" class="q-button q-button--small q-button--secondary pick-color" />
</div>
</div>
<hr class="horizontal-dividing-line" />
<div class="vertical-list-item">
<div>
<h2>撤回后消息是否显示阴影</h2>
<span class="secondary-text">修改将自动保存并实时生效</span>
</div>
<div id="switchShadow" class="q-switch">
<span class="q-switch__handle"></span>
</div>
</div>
<hr class="horizontal-dividing-line" />
<div class="vertical-list-item">
<div>
<h2>撤回后消息下方是否显示“已撤回”提示</h2>
<span class="secondary-text">修改将自动保存并在重新滚动消息后生效</span>
</div>
<div id="switchTip" class="q-switch">
<span class="q-switch__handle"></span>
</div>
</div>
</div>
</div>
</section>
<style>
.img-hidden {
display:none;
}
.path-input {
align-self: normal;
flex: 1;
border-radius: 4px;
margin-right: 16px;
transition: all 100ms ease-out;
}
.path-input:focus {
padding-left: 4px;
}
.bq-icon {
height:16px;
width:16px;
}
/* 通用 */
.config_view {
margin: 20px;
}
.config_view h1 {
color: var(--text_primary);
font-weight: var(--font-bold);
font-size: min(var(--font_size_3), 18px);
line-height: min(var(--line_height_3), 24px);
padding: 0px 16px;
margin-bottom: 8px;
}
.config_view .wrap {
/* Linux样式兼容:--fg_white */
background-color: var(--fill_light_primary, var(--fg_white));
border-radius: 8px;
font-size: min(var(--font_size_3), 18px);
line-height: min(var(--line_height_3), 24px);
margin-bottom: 20px;
overflow: hidden;
padding: 0px 16px;
}
.config_view .vertical-list-item {
margin: 12px 0px;
display: flex;
justify-content: space-between;
align-items: center;
}
.config_view .horizontal-dividing-line {
border: unset;
margin: unset;
height: 1px;
background-color: rgba(127, 127, 127, 0.15);
}
.config_view .vertical-dividing-line {
border: unset;
margin: unset;
width: 1px;
background-color: rgba(127, 127, 127, 0.15);
}
.config_view .ops-btns {
display: flex;
}
.config_view .hidden {
display: none !important;
}
.config_view .disabled {
pointer-events: none;
opacity: 0.5;
}
.config_view .secondary-text {
color: var(--text_secondary);
font-size: min(var(--font_size_2), 16px);
line-height: min(var(--line_height_2), 22px);
margin-top: 4px;
}
.config_view .wrap .title {
cursor: pointer;
font-size: min(var(--font_size_3), 18px);
line-height: min(var(--line_height_3), 24px);
}
.config_view .wrap .title svg {
width: 1em;
height: 1em;
transform: rotate(-180deg);
transition-duration: 0.2s;
transition-timing-function: ease;
transition-delay: 0s;
transition-property: transform;
}
.config_view .wrap .title svg.is-fold {
transform: rotate(0deg);
}
/* 模态框 */
.config_view .modal-window {
display: flex;
justify-content: center;
align-items: center;
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 999;
background-color: rgba(0, 0, 0, 0.5);
}
.config_view .modal-dialog {
width: 480px;
border-radius: 8px;
/* Linux样式兼容:--fg_white */
background-color: var(--bg_bottom_standard, var(--fg_white));
}
.config_view .modal-dialog header {
font-size: 12px;
height: 30px;
line-height: 30px;
text-align: center;
}
.config_view .modal-dialog main {
padding: 0px 16px;
}
.config_view .modal-dialog main p {
margin: 8px 0px;
}
.config_view .modal-dialog footer {
height: 30px;
display: flex;
justify-content: right;
align-items: center;
}
.config_view .modal-dialog .q-icon {
width: 22px;
height: 22px;
margin: 8px;
}
/* 版本号 */
.config_view .versions .wrap {
display: flex;
justify-content: space-between;
padding: 16px 0px;
}
.config_view .versions .wrap>div {
flex: 1;
margin: 0px 10px;
border-radius: 8px;
text-align: center;
}
/* 数据目录 */
.config_view .path .path-input {
align-self: normal;
flex: 1;
border-radius: 4px;
margin-right: 16px;
transition: all 100ms ease-out;
}
.config_view .path .path-input:focus {
padding-left: 5px;
background-color: rgba(127, 127, 127, 0.1);
}
/* 选择框容器 */
.config_view .list-ctl .ops-selects {
display: flex;
gap: 8px;
}
@media (prefers-color-scheme: light) {
.text_color {
color: black;
}
}
@media (prefers-color-scheme: dark) {
.text_color {
color: white;
}
}
</style>
</div>
</body>
`;
const parser = new DOMParser();
const doc2 = parser.parseFromString(new_navbar_item, "text/html");
const node2 = doc2.querySelector("body > div");
//清空消息
node2.querySelector("#clearDb").onclick = async () => {
await window.anti_recall.clearDb();
};
//选择颜色
const pickColor = node2.querySelector(".pick-color");
pickColor.value = nowConfig.mainColor;
pickColor.addEventListener("change", async (event) => {
nowConfig.mainColor = event.target.value;
await window.anti_recall.saveConfig(nowConfig);
});
//存数据库开关
var q_switch_savedb = node2.querySelector("#switchSaveDb");
if (nowConfig.saveDb == null || nowConfig.saveDb == true) {
q_switch_savedb.classList.toggle("is-active");
}
q_switch_savedb.addEventListener("click", async () => {
if (q_switch_savedb.classList.contains("is-active")) {
nowConfig.saveDb = false;
} else {
nowConfig.saveDb = true;
}
q_switch_savedb.classList.toggle("is-active");
await window.anti_recall.saveConfig(nowConfig);
});
//阴影开关
var q_switch_shadow = node2.querySelector("#switchShadow");
if (nowConfig.enableShadow == null || nowConfig.enableShadow == true) {
q_switch_shadow.classList.toggle("is-active");
}
q_switch_shadow.addEventListener("click", async () => {
if (q_switch_shadow.classList.contains("is-active")) {
nowConfig.enableShadow = false;
} else {
nowConfig.enableShadow = true;
}
q_switch_shadow.classList.toggle("is-active");
await window.anti_recall.saveConfig(nowConfig);
});
//提示开关
var q_switch_tip = node2.querySelector("#switchTip");
if (nowConfig.enableTip == null || nowConfig.enableTip == true) {
q_switch_tip.classList.toggle("is-active");
}
q_switch_tip.addEventListener("click", async () => {
if (q_switch_tip.classList.contains("is-active")) {
//取消
nowConfig.enableTip = false;
} else {
//重新设置
nowConfig.enableTip = true;
}
q_switch_tip.classList.toggle("is-active");
await window.anti_recall.saveConfig(nowConfig);
});
view.appendChild(node2);
}
async function patchCss() {
var nowConfig = await window.anti_recall.getNowConfig();
var cssNode = document
.evaluate("/html/head/style[@id='anti-recall-css']", document)
.iterateNext();
if (cssNode) {
cssNode.parentElement.removeChild(cssNode);
}
var stylee = document.createElement("style");
stylee.type = "text/css";
stylee.id = "anti-recall-css";
var sHtml = `.message-content-recalled-parent {
border-radius: 10px;
position: relative;
${nowConfig.enableTip == true ? "margin-bottom: 15px;" : ""}
overflow: unset !important;`;
if (nowConfig.enableShadow == true) {
sHtml += ` margin-top:3px;
margin-left:3px;
margin-right:3px;
box-shadow: 0px 0px 20px 5px ${nowConfig.mainColor};`;
}
sHtml += ` }
.message-content-recalled {
position: absolute;
top: calc(100% + 6px);
font-size: 12px;
white-space: nowrap;
color: var(--text-color);
left: 0;
background-color: var(--background-color-05);
backdrop-filter: blur(28px);
padding: 2px 8px;
border-radius: 6px;
box-shadow: var(--box-shadow);
transition: 300ms;
transform: translateX(-30%);
opacity: 0;
pointer-events: none;
color:${nowConfig.mainColor};
}
`;
stylee.innerHTML = sHtml;
document.getElementsByTagName("head").item(0).appendChild(stylee);
}
export async function onLoad() {
anti_recall.repatchCss(async (event, _) => {
await patchCss();
});
//消息更新回调
anti_recall.recallTip((event, msgId) => {
anti_recall.recallTip(async (event, msgId) => {
console.log("[Anti-Recall]", "尝试反撤回消息ID", msgId);
var oldElement = document.querySelector(
@@ -16,20 +432,24 @@ export function onLoad() {
`.message[id='ml-${msgId}'] .msg-content-container`
);
if (oldElement != null) appendRecalledTag(oldElement);
else if (newElement != null)
appendRecalledTag(newElement.parentElement);
else if (unixElement != null)
appendRecalledTag(unixElement.parentElement);
if (oldElement != null) {
await appendRecalledTag(oldElement);
} else if (newElement != null) {
await appendRecalledTag(newElement.parentElement);
} else if (unixElement != null) {
await appendRecalledTag(unixElement.parentElement);
}
});
//消息列表更新回调
anti_recall.recallTipList((event, msgIdList) => {
anti_recall.recallTipList(async (event, msgIdList) => {
recalledMsgList = msgIdList;
render();
await render();
});
await patchCss();
//监控消息列表,如果有撤回则渲染
const observer = new MutationObserver((mutationsList) => {
const observer = new MutationObserver(async (mutationsList) => {
for (let mutation of mutationsList) {
if (mutation.type === "childList") {
if (
@@ -42,7 +462,7 @@ export function onLoad() {
) {
//是添加的撤回标记,直接忽略
} else {
render();
await render();
}
}
}
@@ -54,10 +474,10 @@ export function onLoad() {
const config = { attributes: true, childList: true, subtree: true };
observer.observe(targetNode, config);
function render() {
async function render() {
//console.log("[Anti-Recall]", "尝试反撤回消息列表", recalledMsgList);
recalledMsgList.forEach((msgId) => {
for (var msgId of recalledMsgList) {
try {
var oldElement = document.querySelector(
`div[id='${msgId}-msgContainerMsgContent']`
@@ -71,56 +491,38 @@ export function onLoad() {
`.message[id='ml-${msgId}'] .msg-content-container`
);
if (oldElement != null) appendRecalledTag(oldElement);
if (oldElement != null) await appendRecalledTag(oldElement);
else if (newElement != null)
appendRecalledTag(newElement.parentElement);
await appendRecalledTag(newElement.parentElement);
else if (unixElement != null)
appendRecalledTag(unixElement.parentElement);
await appendRecalledTag(unixElement.parentElement);
} catch (e) {
console.log("[Anti-Recall]", "反撤回消息时出错", e);
}
});
}
}
function appendRecalledTag(msgElement) {
async function appendRecalledTag(msgElement) {
if (!msgElement) return;
var nowConfig = await window.anti_recall.getNowConfig();
var currRecalledTip = msgElement.querySelector(
".message-content-recalled"
);
if (currRecalledTip == null) {
msgElement.style = `
position: relative;
margin-bottom: 15px;
overflow: unset !important;
`;
const recalledEl = document.createElement("div");
recalledEl.innerText = "已撤回";
recalledEl.classList.add("message-content-recalled");
recalledEl.style = `
position: absolute;
top: calc(100% + 4px);
font-size: 12px;
white-space: nowrap;
color: var(--text-color);
left: 0;
background-color: var(--background-color-05);
backdrop-filter: blur(28px);
padding: 2px 8px;
border-radius: 6px;
box-shadow: var(--box-shadow);
transition: 300ms;
transform: translateX(-30%);
opacity: 0;
pointer-events: none;
color:red;
`;
msgElement.classList.add("message-content-recalled-parent");
msgElement.appendChild(recalledEl);
setTimeout(() => {
recalledEl.style.transform = "translateX(0)";
recalledEl.style.opacity = "1";
}, 5);
if (nowConfig.enableTip == true) {
const recalledEl = document.createElement("div");
recalledEl.innerText = "已撤回";
recalledEl.classList.add("message-content-recalled");
msgElement.appendChild(recalledEl);
setTimeout(() => {
recalledEl.style.transform = "translateX(0)";
recalledEl.style.opacity = "1";
}, 5);
}
} else {
//已经有撤回标记了,不再重复添加
}