// ==UserScript==
// @name NGA优化摸鱼体验插件-自动双向同步数据WebDAV数据
// @namespace https://gist.github.com/nulIptr/b2de9f49cf4f7c3803af9738ad20e1d5
// @version 1.0.0
// @author nuliptr
// @description 通过WebDAV自动同步数据,每次同步还会合并远程数据
// @license MIT
// @match *://bbs.nga.cn/*
// @match *://ngabbs.com/*
// @match *://nga.178.com/*
// @match *://g.nga.cn/*
// @grant unsafeWindow
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
// @run-at document-start
// @inject-into content
// ==/UserScript==
const CHECK_INFO_KEY = 'AutoSyncDataWithMergeRemote_lastCheckInfo';
(function (registerPlugin) {
'use strict';
//去除一些广告
GM_addStyle('#bbs_ads9_add{display:none !important}');
GM_addStyle('#toptopics+span{display:none !important}');
GM_addStyle('.forumbox.postbox+span{display:none !important}');
const AutoSyncDataWithMergeRemote = {
name: 'AutoSyncDataWithMergeRemote',
title: '双向同步数据',
desc: '通过WebDAV自动同步数据',
settings: [
{
key: 'url',
title: 'WebDAV地址',
default: '',
},
{
key: 'username',
title: 'WebDAV账号',
default: '',
},
{
key: 'password',
title: 'WebDAV密码',
default: '',
},
{
key: 'backupFileName',
title: '备份文件名',
default: 'nga_bbs_script_auto_sync_rolling.json',
},
{
key: 'checkInterval',
title: '检查间隔(单位:分)',
default: 60,
},
],
buttons: [
{
title: '检查连接',
action: 'testConnections',
},
{
title: '备份',
action: 'backup',
},
{
title: '还原',
action: 'restore',
},
],
lastCheckInfo: {},
beforeSaveSettingFunc(settings) {
if (settings['checkInterval'] < 0) {
return '检查间隔不能小于0。';
}
if (settings['keywordsListFileName'] == '') {
return '关键词列表文件名不能为空。';
}
if (settings['banListFileName'] == '') {
return '黑名单列表文件名不能为空。';
}
if (settings['markListFileName'] == '') {
return '标记名单列表文件名不能为空。';
}
if (settings['metaFileName'] == '') {
return '元数据文件名不能为空。';
}
},
initFunc() {
if (typeof this.pluginSettings['backupFileName'] !== 'string' || this.pluginSettings['backupFileName'] === '') {
this.pluginSettings['backupFileName'] = 'nga_bbs_script_auto_sync_rolling.json';
}
if (typeof this.pluginSettings['checkInterval'] !== 'number' || this.pluginSettings['checkInterval'] < 0) {
this.pluginSettings['checkInterval'] = 60;
}
try {
this.lastCheckInfo = JSON.parse(this.mainScript.getValue(CHECK_INFO_KEY) ?? '{}');
} catch (e) {
this.printLog('读取上次检查信息失败', 'err');
console.error(e);
}
},
postProcFunc(...args) {
console.log(args, 'postProcFunc(...args) {');
if (this.pluginInputs['url'].val()) {
const _this = this;
const handler = async () => {
const lastCheckTime = _this.lastCheckInfo['lastCheckTime'];
try {
if (typeof lastCheckTime !== 'number' || lastCheckTime + _this.pluginSettings['checkInterval'] * 60 * 1000 < Date.now()) {
await _this.sync(true);
_this.lastCheckInfo['lastCheckTime'] = Date.now();
}
} catch (e) {
_this.mainScript.printLog('检查连接失败');
console.log(e);
} finally {
try {
_this.mainScript.setValue(CHECK_INFO_KEY, JSON.stringify(_this.lastCheckInfo));
} catch (e) {
_this.mainScript.printLog('保存检查信息失败');
console.error(e);
}
setTimeout(handler, 1000);
}
};
this.timeout = setTimeout(handler, 1000);
}
},
// 请求构造
request({ method, path = '', headers, ...config }) {
// 获取输入框的当前的值
let url = this.pluginInputs['url'].val().trim();
url[url.length - 1] !== '/' && (url += '/');
const username = this.pluginInputs['username'].val().trim();
const password = this.pluginInputs['password'].val().trim();
this.buttons.forEach((button) => button.$el.attr('disabled', true));
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method,
url: url + path,
headers: {
authorization: 'Basic ' + btoa(`${username}:${password}`),
'Cache-control': 'no-cache',
...headers,
},
...config,
onload: (response) => {
this.buttons.forEach((button) => button.$el.removeAttr('disabled'));
if (response.status >= 200 && response.status < 300) {
resolve(response);
} else {
if (method == 'GET' && response.status == 404) {
resolve(null);
return;
}
this.mainScript.popMsg(`WebDAV请求失败! 状态码: ${response.status} ${response.statusText}`, 'err');
}
},
onerror: (error) => {
reject(error);
this.buttons.forEach((button) => button.$el.removeAttr('disabled'));
this.mainScript.popMsg(`WebDAV请求失败!${error}`);
},
});
});
},
// 获取文件列表
getFileList() {
return new Promise((resolve, reject) => {
this.request({
method: 'PROPFIND',
headers: { depth: 1 },
}).then((res) => {
let files = [];
let path = res.responseText.match(/(?<=<d:href>).*?(?=<\/d:href>)/gi);
path.forEach((p) => {
const filename = p.split('/').pop();
files.push(filename);
});
resolve(files);
});
});
},
// 下载配置
async restore() {
const filename = this.pluginSettings['backupFileName'];
const res = await this.request({
method: 'GET',
path: filename,
});
this.mainScript.getModule('BackupModule').import(res.responseText, false);
},
// 测试连通性
async testConnections() {
await this.getFileList();
this.mainScript.popMsg('连接成功!同步配置看起来没问题');
},
// 上传配置
async sync() {
const local = this.mainScript.getModule('BackupModule').export('*', false);
const filename = this.pluginSettings['backupFileName'];
let remote = null;
try {
let res = await this.request({
method: 'GET',
path: filename,
});
remote = res.responseText;
} catch (error) {}
const next = this.merge(local, remote);
this.mainScript.getModule('BackupModule').import(next, false);
await this.request({
method: 'PUT',
path: filename,
data: next,
});
},
merge(local, remote) {
if (remote === null) {
return local;
}
const l = JSON.parse(local);
const r = JSON.parse(remote);
//merge keywords_list
(function () {
const set = new Set(l.keywords_list);
r.keywords_list.forEach((v) => {
set.add(v);
});
l.keywords_list = Array.from(set);
})();
//merge ban_list
(function () {
const set = new Set(l.ban_list.map((v) => v.uid));
r.ban_list.forEach((v) => {
if (!set.has(v.uid)) {
l.ban_list.push(v);
}
});
})();
//merge mark_list
(function () {
const set = new Set(l.mark_list.map((v) => v.uid));
r.mark_list.forEach((v) => {
if (!set.has(v.uid)) {
l.mark_list.push(v);
}
});
})();
return JSON.stringify(l);
},
};
registerPlugin(AutoSyncDataWithMergeRemote);
})(function (plugin) {
plugin.meta = GM_info.script;
unsafeWindow.ngaScriptPlugins = unsafeWindow.ngaScriptPlugins || [];
unsafeWindow.ngaScriptPlugins.push(plugin);
});