// ==UserScript==
// @name QuickPatrol_v2
// @namespace qp_tool_v2
// @version 1.97
// @description MediaWiki巡查工具 | A patrol tool for MediaWiki
// @author teaSummer
// @match *://*/wiki/*
// @match *://*/w/*
// @match *://*/index.php?*
// @license MIT
// @grant none
// ==/UserScript==
(function () {
'use strict';
// 配置 | Configuration
const config = {
rollbackMode: 'summary', // 回退模式,值为'summary'、'confirm'、'd+s'、'c+s'或'default' | Rollback Mode, values are 'summary', 'confirm', 'd+s', 'c+s' or 'default'
usingOOUI: true, // 使用OOUI,值为布尔值或'force' | Using OOUI, values are booleans or 'force'
maxRetries: 10 // 获取mwApi最大重试次数 | Maximum Retries of Getting mwApi
}
// 本地化 | Localization
const w = {
'en': {
un: 'This edit has not yet been patrolled',
ing: 'Quick patrolling...',
done: 'Quick patrolled',
t_un: 'Unpatrolled',
g_un: 'Unpatrolled',
hl: 'Highlighted: ',
g_cr_1: 'Sure to rollback this edit?',
g_er_1: 'Additional rollback summary:',
g_er_2: 'Revert edits by [[Special:Contribs/$1|$1]] ([[User talk:$1|talk]])',
g_er_3: 'Edit rollback summary',
c: ': '
},
'zh-hans': {
un: '该编辑尚未巡查',
ing: '快速巡查中…',
done: '已快速巡查',
t_un: '未巡查',
g_un: '尚未巡查',
hl: '已高亮:',
g_cr_1: '确定要回退此编辑吗?',
g_er_1: '附加回退摘要:',
g_er_2: '回退[[Special:Contribs/$1|$1]]([[User talk:$1|留言]])所做的编辑',
g_er_3: '编辑回退摘要',
c: ':'
},
'zh-hant': {
un: '該編輯尚未巡查',
ing: '快速巡查中…',
done: '已快速巡查',
t_un: '未巡查',
g_un: '尚未巡查',
hl: '已明顯標示:',
g_cr_1: '確定要回退此編輯嗎?',
g_er_1: '附加回退摘要:',
g_er_2: '回退[[Special:Contribs/$1|$1]]([[User talk:$1|留言]])所做的編輯',
g_er_3: '編輯回退摘要',
c: ':'
},
}
let mwApi, mwLang, rights, l, fail = 0, load = false;
const need_patrol = '.mw-changeslist-reviewstatus-unpatrolled:not(.mw-rcfilters-ui-highlights-enhanced-toplevel):not(.quickpatrol), .revisionpatrol-unpatrolled';
function g_cr() {
$('.mw-rollback-link a:not(.quickpatrol-rollback)').each(function () {
const href = $(this).attr('href');
$(this).click((e) => {
e.preventDefault();
if (config.usingOOUI) {
OO.ui.confirm(l.g_cr_1).then((confirmed) => {
if (confirmed) location.href = href;
});
} else {
if (confirm(l.g_cr_1)) location.href = href;
}
});
}).addClass('quickpatrol-rollback').addClass(mwLang.startsWith('zh-') ? 'consolas' : '');
}
function g_er(p) {
let el = '.mw-rollback-link a';
if (p) {
if (p == 2) g_cr();
$(el).each(function () {
$(this).after($(`<span class="edit-rollback" href="${$(this).attr('href')}" title="${l.g_er_3}"></span>`));
}).addClass('quickpatrol-rollback').addClass(mwLang.startsWith('zh-') ? 'consolas' : '');
el = '.edit-rollback';
}
el = el + ':not(.quickpatrol-rollback)';
$(el).each(function () {
const href = $(this).attr('href');
$(this).click((e) => {
e.preventDefault();
const rb = (result) => {
const r = result.trim();
location.href = href + '&summary=' + encodeURIComponent(r ? summary + l.c + r : summary);
}
const name = decodeURIComponent(href.match(/&from=(.+)&token/)[1].replace(/\+/g, ' '));
const summary = mw.format(l.g_er_2, name);
if (config.usingOOUI) {
OO.ui.prompt(l.g_er_1).then((result) => {
rb(result);
});
} else {
const result = prompt(l.g_er_1);
if (result != null) rb(result);
}
});
}).addClass('quickpatrol-rollback').addClass(mwLang.startsWith('zh-') ? 'consolas' : '');
}
async function rollback_gadget() {
if (config.rollbackMode == 'default' || !load) return;
if (!(OO && OO.ui && OO.ui.confirm)) {
if (config.usingOOUI == 'force') {
await mw.loader.load('https://fastly.jsdelivr.net/npm/oojs-ui/dist/oojs-ui-windows-wikimediaui.min.css', 'text/css');
await mw.loader.load('https://fastly.jsdelivr.net/npm/oojs-ui/dist/oojs-ui-windows.min.js');
} else {
// 不强制使用:在不兼容时自动禁用
config.usingOOUI = false;
}
}
load[0](load[1]);
}
async function init() {
if (fail >= config.maxRetries) return;
try {
mwApi = await new mw.Api();
mwLang = Object.keys(await mw.language.data)[0];
if (['zh-cn', 'zh-hans', 'zh-hans-cn', 'zh', 'cn'].indexOf(mwLang) != -1) mwLang = 'zh-hans';
else if (mwLang.startsWith('zh-')) mwLang = 'zh-hant';
if (Object.keys(w).indexOf(mwLang) == -1) mwLang = 'en';
l = w[mwLang];
if (!load) {
load = true;
const oe = ['.mw-changeslist', '#mw-diff-ntitle1 strong', '#mw-diff-otitle1 strong', '.mw-contributions-list li'];
const ob_IPE = new MutationObserver(with_IPE);
const ob = new MutationObserver((ml) => {
ml.forEach((m) => {
const t = $(m.target);
if (t.hasClass('mw-changeslist') || t.attr('class').indexOf('revisionpatrol-') != -1) main();
if (m.addedNodes.length) {
const e = $(m.addedNodes);
if (e.hasClass('quick-diff')) ob_IPE.observe(e.find('.ipe-progress')[0], { attributes: true });
}
});
});
ob.observe(document.body, { childList: true });
for (const x of oe) if ($(x).length) ob.observe($(x)[0], { childList: true, attributes: true });
await mw.loader.load(`https://cdn.jsdelivr.net/gh/teaSummer/QuickPatrol_v2@main/styles.css`, 'text/css');
await mw.loader.load(`https://fastly.jsdelivr.net/gh/teaSummer/QuickPatrol_v2@minecraft-wiki/patrol/${mwLang}/Gadget-revisionPatrol.css`, 'text/css');
await mw.loader.load(`https://fastly.jsdelivr.net/gh/teaSummer/QuickPatrol_v2@minecraft-wiki/patrol/${mwLang}/Gadget-revisionPatrol.js`);
if (config.rollbackMode == 'summary') load = [g_er];
if (config.rollbackMode == 'confirm') load = [g_cr];
if (config.rollbackMode == 'd+s') load = [g_er, 1];
if (config.rollbackMode == 'c+s') load = [g_er, 2];
if (load.length == 2) await mw.loader.load(`https://fastly.jsdelivr.net/gh/teaSummer/QuickPatrol_v2@minecraft-wiki/rollback/${mwLang}/Gadget-editableRollback.css`, 'text/css');
}
} catch (e) {
fail = fail + 1;
if (fail < config.maxRetries) console.warn(`[QuickPatrol] Failed to call MediaWiki. Retrying... (${fail}/${config.maxRetries})`);
else console.error(`[QuickPatrol] Failed to call MediaWiki. (${fail}/${config.maxRetries})`);
new Promise(() => setTimeout(init, 2000));
return;
}
console.log('[QuickPatrol] Checking rights...');
rights = (
await mwApi.get({
action: 'query',
meta: 'userinfo',
uiprop: 'rights'
})
).query.userinfo.rights;
main();
return;
}
function with_IPE() {
$('.diff-version:not(.diff-hidden-history)').click(function () {
const me = $(this);
if (me.attr('title')) return;
const value = me.text().replace(/[^0-9]/g, '');
me.addClass('patrolling').attr('title', l.ing);
patrol(me, value);
return value;
});
}
async function main() {
if (!rights) {
if (fail >= config.maxRetries || !fail) init();
return;
}
if (rights.includes('rollback')) rollback_gadget();
if (rights.includes('patrol')) {
$(need_patrol).attr('data-mw-revid', function (_i, value) {
const that = $(this);
that.find('.revisionpatrol-icon-unpatrolled').after(`<span class="unpatrolled quickpatrol-icon-unpatrolled" title="${l.g_un}">!</span>`);
that.find('.revisionpatrol-icon-unpatrolled').remove();
that.find('.unpatrolled').addClass('quickpatrol').click(async function () {
const me = $(this);
me.addClass('patrolling').removeClass('unpatrolled');
if (me.text() == '!') {
$(this).text('#').attr('title', l.ing);
if (!value && that.attr('data-mw-logid')) {
try {
value = new RegExp(`"logid":${that.attr('data-mw-logid')},.+?,"revid":([0-9]+)`).exec(JSON.stringify((await mwApi
.get({
action: 'query',
list: 'logevents',
leprop: 'ids',
letitle: that.find('td.mw-enhanced-rc-nested').attr('data-target-page'),
letype: that.attr('data-mw-logaction').split('/')[0],
lelimit: 'max',
format: 'json'
})
)))[1];
} catch (e) {
$(this).text('!').removeClass('patrolling').attr('title', l.un);
return;
}
}
patrol(me, value, () => {
that.removeClass('mw-rcfilters-highlight-color-c5').removeClass('mw-changeslist-reviewstatus-unpatrolled');
that.attr('title', that.attr('title').replace(new RegExp(`(, |、\u200B)?${l.t_un}(、\u200B|, )?`, 'g'), ''));
if (that.attr('title') == l.hl) that.removeAttr('title');
});
}
});
return value;
});
}
}
function patrol(me, revid, successFallback = () => { }, exFailFallback = () => { }) {
const failFallback = () => {
console.warn(`[QuickPatrol] FAILED (revid: ${value})`);
me.text('!').removeClass('patrolling').removeClass('unpatrolled').attr('title', l.un);
if (me.hasClass('quickpatrol-icon-unpatrolled')) me.attr('title', l.g_un);
exFailFallback();
}
console.debug(`[QuickPatrol] TRYING (revid: ${revid})`);
mwApi.get({
action: 'query',
meta: 'tokens',
type: 'patrol',
format: 'json'
}).done((data) => {
mwApi.post({
action: 'patrol',
revid: revid,
token: data.query.tokens.patroltoken,
format: 'json'
}).done(() => {
console.debug(`[QuickPatrol] SUCCEEDED (revid: ${revid})`);
me.text('✔').removeClass('patrolling').removeClass('unpatrolled').attr('title', l.done);
successFallback();
}).fail(failFallback);
}).fail(failFallback);
}
init();
})();