QuickPatrol_v2

MediaWiki巡查工具 | A patrol tool for MediaWiki

// ==UserScript==
// @name         QuickPatrol_v2
// @namespace    qp_tool_v2
// @version      2.05
// @description  MediaWiki巡查工具 | A patrol tool for MediaWiki
// @author       teaSummer
// @match        *://*/wiki/*
// @match        *://*/w/*
// @match        *://*/index.php?*
// @license      MIT
// @grant        none
// ==/UserScript==

(() => {
    'use strict';

    // 配置 | Configuration
    const config = {
        patrol: true, // 快速巡查 | Quick Patrol
        rollback: true, // 快速回退 | Quick Rollback
        rollbackMode: 'summary', // 回退模式,值为'summary'、'confirm'、'd+s'、'c+s'或'default' | Rollback Mode, values are 'summary', 'confirm', 'd+s', 'c+s' or 'default'
        abuseEdit: true, // 过滤内容编辑助手 | Helper that Filtered Edits
        _blank: false, // 在新标签页打开链接 | Links Open in New Tabs
        usingOOUI: true, // 使用OOUI | Using OOUI
        maxRetries: 10 // 初始化最大重试次数 | Maximum Retries of Initialization
    }

    // 本地化 | 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: ',
            s: 'Additional summary:',
            h_1: 'Apply',
            h_2: '[[Special:AbuseLog/$1|AbuseLog/$1]] by [[Special:Contribs/$2|$2]] ([[User talk:$2|talk]]), ',
            h_3: 'the original summary is "$1"',
            h_4: 'no original summary',
            g_cr_1: 'Sure to rollback this edit?',
            g_er_1: 'Revert edits by [[Special:Contribs/$1|$1]] ([[User talk:$1|talk]])',
            g_er_2: 'Edit rollback summary',
            c: ': '
        },
        'zh-hans': {
            un: '该编辑尚未巡查',
            ing: '快速巡查中…',
            done: '已快速巡查',
            t_un: '未巡查',
            g_un: '尚未巡查',
            hl: '已高亮:',
            s: '附加摘要:',
            h_1: '应用',
            h_2: '[[Special:AbuseLog/$1|滥用日志/$1]] — [[Special:Contribs/$2|$2]]([[User talk:$2|留言]]),',
            h_3: '原始摘要为“$1”',
            h_4: '没有原始摘要',
            g_cr_1: '确定要回退此编辑吗?',
            g_er_1: '回退[[Special:Contribs/$1|$1]]([[User talk:$1|留言]])所做的编辑',
            g_er_2: '编辑回退摘要',
            c: ':'
        },
        'zh-hant': {
            un: '該編輯尚未巡查',
            ing: '快速巡查中…',
            done: '已快速巡查',
            t_un: '未巡查',
            g_un: '尚未巡查',
            hl: '已明顯標示:',
            s: '附加摘要:',
            h_1: '應用',
            h_2: '[[Special:AbuseLog/$1|濫用日誌/$1]] — [[Special:Contribs/$2|$2]]([[User talk:$2|留言]]),',
            h_3: '原始摘要為「$1」',
            h_4: '沒有原始摘要',
            g_cr_1: '確定要回退此編輯嗎?',
            g_er_1: '回退[[Special:Contribs/$1|$1]]([[User talk:$1|留言]])所做的編輯',
            g_er_2: '編輯回退摘要',
            c: ':'
        }
    }

    const $E = (e, f, r) => {
        if ($(e).length) return typeof f == 'function' ? f($(e)) : $(e);
        return typeof f == 'function' ? r : f;
    }
    let mwApi, mwLang, status = {}, rights, l, fail = 0, load = false;
    status.np = '.mw-changeslist-reviewstatus-unpatrolled:not(.mw-rcfilters-ui-highlights-enhanced-toplevel):not(.quickpatrol), .revisionpatrol-unpatrolled';
    status.load = async (...l) => {
        for (const f of l) await mw.loader.load(`https://fastly.jsdelivr.net/gh/teaSummer/QuickPatrol_v2@${f}`, f.endsWith('.css') ? 'text/css' : void 0);
    }

    function helper() {
        const g = (e) => $E(`.mw-abuselog-details-${e} div.mw-abuselog-var-value`, (e) => e.text().replace(/^'|'$/g, ''));
        if ($E('.mw-abuselog-details')) {
            const v = {
                user_name: g('user_name'),
                action: g('action'),
                summary: g('summary'),
                new_wikitext: g('new_wikitext'),
                page_prefixedtitle: g('page_prefixedtitle'),
                lgid: location.href.replace(/\/+$/, '').split('/').slice(-1)[0].replace(/[^0-9]/g, '')
            }
            if (v.action == 'edit') {
                let summary = l.h_2.replace(/\$1/g, v.lgid).replace(/\$2/g, v.user_name);
                $('#mw-content-text p').after(`<button class="quickpatrol-abuseedit">${l.h_1}</button>`);
                summary = summary + (v.summary ? l.h_3.replace('$1', v.summary) : l.h_4);
                if (v.summary.trim()) {
                    $('.quickpatrol-abuseedit').after(`<div class="quickpatrol-abuseedit-summary"/>`);
                    $('.quickpatrol-abuseedit-summary').text(v.summary);
                }
                $E('.diff-type-table', $('.quickpatrol-abuseedit-summary')).after('<textarea class="new_wikitext" cols="80" rows="25"/>');
                $('.new_wikitext').text(v.new_wikitext).before('<input class="page_prefixedtitle"/>');
                $('.page_prefixedtitle').val(v.page_prefixedtitle).attr('placeholder', v.page_prefixedtitle);
                $('.mw-abuselog-details').wrap(`<details/>`);
                $('.quickpatrol-abuseedit').click(function () {
                    const me = $(this);
                    const rb = (result) => {
                        const r = result.trim();
                        me.attr('disabled', true);
                        mwApi.edit($E('.page_prefixedtitle', (e) => e.text() ? e.text() : v.page_prefixedtitle, v.page_prefixedtitle), () => Object({
                            summary: r ? summary + l.c + r : summary,
                            text: $E('.new_wikitext', (e) => e.text(), v.new_wikitext),
                            minor: true
                        })).done(() => me.remove()).fail(() => me.removeAttr('disabled'));
                    }
                    if (config.usingOOUI) OO.ui.prompt(l.s).then((result) => rb(result));
                    else {
                        const result = prompt(l.s);
                        if (result != null) rb(result);
                    }
                });
            }
        }
    }

    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;
            });
            status.rf(this);
        });
    }

    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_2}"></span>`));
                status.rf(this);
            })
            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_1, name);
                if (config.usingOOUI) OO.ui.prompt(l.s).then((result) => rb(result));
                else {
                    const result = prompt(l.s);
                    if (result != null) rb(result);
                }
            });
            status.rf(this);
        });
    }

    async function rollback_gadget() {
        if (config.rollbackMode == 'default' || !load) return;
        if (!(OO && OO.ui && OO.ui.confirm)) 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'].includes(mwLang)) mwLang = 'zh-hans';
            else if (mwLang.startsWith('zh-')) mwLang = 'zh-hant';
            if (!Object.keys(w).includes(mwLang)) 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);
                let z = false;
                const ob = new MutationObserver((ml) => {
                    ml.forEach((m) => {
                        const t = $(m.target);
                        if (!z && t.attr('class').includes('revisionpatrol-')) {
                            z = true;
                            main();
                        }
                        if (t.hasClass('mw-changeslist')) main();
                        $E(m.addedNodes, (e) => {
                            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) $E(x, (e) => ob.observe(e[0], { childList: true, attributes: true }));
                status.load('main/styles.css', `minecraft-wiki/patrol/${mwLang}/Gadget-revisionPatrol.css`, `minecraft-wiki/patrol/${mwLang}/Gadget-revisionPatrol.js`);
                load = ((m) => m == 'summary' ? [g_er] : m == 'confirm' ? [g_cr] : m == 'd+s' ? [g_er, 1] : m == 'c+s' ? [g_er, 2] : [])(config.rollbackMode);
                if (load.length == 2) status.load(`minecraft-wiki/rollback/${mwLang}/Gadget-editableRollback.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;

        Object.assign(status, {
            patrol: rights.includes('patrol') && config.patrol,
            rollback: rights.includes('rollback') && config.rollback,
            abuselog: rights.includes('abusefilter-log-detail') && config.abuseEdit,
            rf: (e) => $(e).addClass('quickpatrol-rollback' + (mwLang.startsWith('zh-') ? ' consolas' : ''))
        });
        if (config._blank) $('.interlanguage-link-target, .vector-menu-content .selected a, #p-personal a, #mw-panel a:not([href^="#"]), a:not([href^="#"],[href$="/doc"],[href*="section="],:has(span))').attr('target', '_blank');

        main();
    }

    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);
        });
    }

    async function main() {
        if (!rights) {
            if (fail >= config.maxRetries || !fail) init();
            return;
        }
        if (status.abuselog) helper();
        if (status.rollback) rollback_gadget();
        if (status.patrol) {
            $(status.np).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 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');
                        });
                    }
                });
            });
        }
    }

    function patrol(me, revid, success = () => { }, exFail = () => { }) {
        const fail = () => {
            console.warn(`[QuickPatrol] FAILED (revid: ${value})`);
            me.text('!').removeClass('patrolling unpatrolled').attr('title', l.un);
            if (me.hasClass('quickpatrol-icon-unpatrolled')) me.attr('title', l.g_un);
            exFail();
        }
        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('✔').addClass('quickpatrol').removeClass('patrolling unpatrolled').attr('title', l.done);
                success();
            }).fail(fail);
        }).fail(fail);
    }

    init();
})();