LLWiki正在建设中,欢迎加入我们

MediaWiki:Gadget-BatchRollback.js

来自LLWiki
跳转到导航 跳转到搜索

注意:在保存之后,您可能需要清除浏览器缓存才能看到所作出的变更的影响。

  • Firefox或Safari:按住Shift的同时单击刷新,或按Ctrl-F5Ctrl-R(Mac为⌘-R
  • Google Chrome:Ctrl-Shift-R(Mac为⌘-Shift-R
  • Internet Explorer:按住Ctrl的同时单击刷新,或按Ctrl-F5
  • Opera:前往菜单 → 设置(Mac为Opera → Preferences),然后隐私和安全 → 清除浏览数据 → 缓存的图片和文件
/**
 * @Function: 批量回退一名用户的编辑,并可以同时删除对应的历史版本
 * @Dependencies: oojs-ui-core, ext.gadget.site-lib, mediawiki.api, jquery.makeCollapsible
 * @Author: [[User:Leranjun]]
 * @EditedBy: [[User:Bhsd]]
 */
"use strict";
/* global OO, wgULS */
(() => {
    mw.messages.set( wgULS({
        'gadget-br-vandal': '破坏', 'gadget-br-content': '内容', 'gadget-br-user': '用户名', 'gadget-br-time': '时间',
        'gadget-br-end': '请输入某次编辑的版本编号或时间戳,或使用 “+” 按钮选取一次编辑', 'gadget-br-contribs': '用户贡献列表',
        'gadget-br-hide': '隐藏', 'gadget-br-hide-help': '一般不需要隐藏用户名', 'gadget-br-error': '错误:无法获得$1',
        'gadget-br-start': '开始批量回退,回溯至 $1', 'gadget-br-invalid': '版本编号或时间戳的格式不正确!',
        'gadget-br-complete': '批量回退完成,请检查日志,并在刷新页面后检查未删除的贡献有无遗漏或过度回退',
        'gadget-br-continue': '本次参数:uccontinue = "$1"', 'gadget-br-nuke': '本次发现 $1 个该用户创建的新页面,请前往' +
        '<a href="/zh/special:nuke/$2" target="_blank">Special:大量删除</a>进行删除', 'gadget-br-rollback': '进入回退阶段',
        'gadget-br-count': '本次获取 $1 条待删除贡献和 $2 个待回退页面', 'gadget-br-noneed': '本次没有需要处理的贡献',
        'gadget-br-error-rollback-count': '本次共有 $1 个页面回退失败<div class="mw-collapsible-content">$2</div>',
        'gadget-br-error-rollback': '页面<a class="BatchRollback-link">$1</a>,错误:$2', 'gadget-br-del': '进入删除阶段',
        'gadget-br-error-revdel-count': '本次共有 $1 组(合计 $2 次)编辑删除失败,您可以刷新页面后查看未删除的贡献' +
        '<div class="mw-collapsible-content">$3</div>', 'gadget-br-error-revdel': '版本 “$1”,错误:$2',
        'gadget-br-warn': '警告!批量回退如果误操作可能造成严重后果,您确定要执行吗?'
    }, {
        'gadget-br-vandal': '破壞', 'gadget-br-content': '內容', 'gadget-br-user': '使用者名稱', 'gadget-br-time': '時間',
        'gadget-br-end': '請輸入某次編輯的版本編號或時間戳,或使用 “+” 按鈕選取一次編輯', 'gadget-br-contribs': '使用者貢獻列表',
        'gadget-br-hide': '隱藏', 'gadget-br-hide-help': '一般不需要隱藏使用者名稱', 'gadget-br-error': '錯誤:無法獲得$1',
        'gadget-br-start': '開始批次還原,回溯至 $1', 'gadget-br-invalid': '版本編號或時間戳的格式不正確!',
        'gadget-br-complete': '批次還原完成,請檢查日誌,並在重新整理頁面後檢查未刪除的貢獻有無遺漏或過度還原',
        'gadget-br-continue': '本次參數:uccontinue = "$1"', 'gadget-br-nuke': '本次發現 $1 個該使用者創建的新頁面,請前往' +
        '<a href="/zh/special:nuke/$2" target="_blank">Special:大量刪除</a>進行刪除', 'gadget-br-rollback': '進入還原階段',
        'gadget-br-count': '本次獲取 $1 條待刪除貢獻和 $2 個待還原頁面', 'gadget-br-noneed': '本次沒有需要處理的貢獻',
        'gadget-br-error-rollback-count': '本次共有 $1 個頁面還原失敗<div class="mw-collapsible-content">$2</div>',
        'gadget-br-error-rollback': '頁面<a class="BatchRollback-link">$1</a>,錯誤:$2', 'gadget-br-del': '進入刪除階段',
        'gadget-br-error-revdel-count': '本次共有 $1 組(合計 $2 次)編輯刪除失敗,您可以重新整理頁面後檢視未刪除的貢獻' +
        '<div class="mw-collapsible-content">$3</div>', 'gadget-br-error-revdel': '版本 “$1”,錯誤:$2',
        'gadget-br-warn': '警告!批次還原如果誤操作可能造成嚴重後果,您確認要執行嗎?'
    }) );
    const api = new mw.Api(),
        ucuser = mw.config.get( 'wgRelevantUserName' ),
        // 可能外面套一层max-height: 100vh; overflow-y: auto;的div容器会好一点?但需要考虑是否自动向下滚动
        $log = $('<table>', {id: 'rollback-log'}).on('click', '.BatchRollback-link', function() {
        const $this = $(this);
        $this.attr({target: '_blank', href: mw.util.getUrl( $this.text() )});
    }),
        params = {list: "usercontribs", ucuser, uclimit: "max", ucprop: 'ids|title|flags'},
        log = (...args) => {
        const $td = $('<td>', {html: mw.msg(...args)});
        if (/gadget-br-error-\w+-count/.test( args[0] )) { $td.makeCollapsible( {collapsed: true} ); }
        $('<tr>', {html: [$('<td>', {text: new Date().toLocaleTimeString( 'ia' )}), $td]}).appendTo( $log );
    },
        allSettled = (ele, i) => i < 5 ? {status: 'rejected', reason: '调试'} : {status: 'fulfilled'},
        // 分批处理API返回的贡献列表,防止浏览器内存溢出
        batchQuery = (ucend, dry, uccontinue) => {
        const reason = reasonBox.getValue() || undefined,
            summary = `批量回退:${reason || '理由未填写'}`,
            hide = suppressBox.getValue().join( '|' );
        if (uccontinue) { log('gadget-br-continue', uccontinue); } // 记录这一批次的参数,以供出错时重新处理
        return mw.timedQuery(api, $.extend({ucend, uccontinue}, params), '用户贡献').then(data => {
            const ucAll = data.query.usercontribs.filter(ele => !ele.texthidden),
                nNew = ucAll.filter(ele => ele.new).length,
                uc = ucAll.filter(ele => !ele.new),
                more = (data.continue || {}).uccontinue,
                tops = uc.filter(ele => ele.top).map(ele => ele.title),
                queries = [...new Set( uc.map(ele => ele.pageid) )].map(pageid => {
                const revids = uc.filter(ele => ele.pageid == pageid).map(ele => ele.revid),
                    n = 50; // 单次API访问至多删除50条贡献(未验证)
                return Array( Math.ceil(revids.length / n) ).fill()
                    .map((ele, i) => revids.slice(i * n, i * n + n).join( '|' ));
            }).flat();
            if (nNew) { log('gadget-br-nuke', nNew, ucuser); } // 记录新页面数量
            if (uc.length) { log( 'gadget-br-count', uc.length, tops.length ); } // 记录待处理的编辑和页面数
            else { log( 'gadget-br-noneed' ); } // 记录没有需要处理的编辑
            if (tops.length) { log( 'gadget-br-rollback' ); } // 记录回退开始
            return (dry ? Promise.resolve( tops.map( allSettled ) ) :
                Promise.allSettled( tops.map(ele => api.rollback(ele, ucuser, {summary})) )).then(records => {
                const rejected = records.map((ele, i) => $.extend(ele, {title: tops[i]}))
                    .filter(ele => ele.status == 'rejected');
                if (rejected.length) { // 记录回退失败的页面,默认折叠
                    log( 'gadget-br-error-rollback-count', rejected.length,
                        rejected.map(ele => mw.msg('gadget-br-error-rollback', ele.title, ele.reason)).join( '<br>' )
                    );
                }
                if (uc.length) { log( 'gadget-br-del' ); } // 记录删除贡献开始
                return (dry ? Promise.resolve( queries.map( allSettled ) ) :
                    Promise.allSettled( queries.map(ids => api.postWithEditToken({
                    action: "revisiondelete", type: "revision", ids, hide, reason
                })) )).then(records => {
                    const rejected = records.map((ele, i) => $.extend(ele, {ids: queries[i]}))
                        .filter(ele => ele.status == 'rejected');
                    if (rejected.length) { // 记录删除贡献失败的编辑,默认折叠
                        log( 'gadget-br-error-revdel-count', rejected.length,
                            rejected.map(ele => ele.ids.split( '|' )).flat().length,
                            rejected.map(ele => mw.msg('gadget-br-error-revdel',
                                ele.ids.replaceAll('|', '|<wbr>'), ele.reason)).join( '<br>' )
                        );
                    }
                    return more ? batchQuery(ucend, dry, more) : null;
                });
            });
        }, () => { throw null; });
    },
        // 预处理函数
        main = (dry) => {
        timeBox.getValidity().then(() => {
            $log.empty();
            submitBtn.setDisabled( true );
            dryBtn.setDisabled( true );
            const revids = timeBox.getValue();
            (/^\d+$/.test( revids ) ? mw.timedQuery(api, {prop: 'revisions', rvprop: 'timestamp', revids}, '编辑时间戳')
                .then(data => data.query.pages[0].revisions[0].timestamp, () => { throw null; }) :
                Promise.resolve( revids )).then(limit => {
                log('gadget-br-start', limit); // 记录批量回退开始
                batchQuery( limit, dry ).then(() => { log('gadget-br-complete'); }, // 记录批量回退完成
                    () => { log('gadget-br-error', mw.msg( 'gadget-br-contribs' )); }); // 记录无法获取贡献列表
            }, () => { log('gadget-br-error', mw.msg( 'gadget-br-timestamp' )); }); // 记录无法获取时间戳
            submitBtn.setDisabled( false );
            dryBtn.setDisabled( false );
        }, () => { mw.notify(mw.msg( 'gadget-br-invalid' ), {type: 'error', tag: 'BatchRollback'}); });
    },
    
        // 操作界面,直接使用OO.ui.PanelLayout的样式
        timeBox = new OO.ui.TextInputWidget({ inputFilter: (str) => str.trim(), // jshint ignore: line
        icon: 'articleAdd', validate: /^(\d+|\d{4}-\d\d-\d\dT\d\d:\d\d:\d\dZ)$/ // 这里严格一点,不接受随意填写的时间戳
    }),
        reasonBox = new OO.ui.TextInputWidget({value: mw.msg( 'gadget-br-vandal' )}), // jshint ignore: line
        suppressBox = new OO.ui.CheckboxMultiselectInputWidget({options: [ // jshint ignore: line
        {data: 'content', label: mw.msg( 'gadget-br-content' ), disabled: true},
        {data: 'comment', label: '摘要'},
        {data: 'user', label: mw.msg( 'gadget-br-user' )}
    ], value: ['content']}),
        submitBtn = new OO.ui.ButtonWidget({ label: mw.msg( 'ooui-dialog-message-accept' ), // jshint ignore: line
        flags: ["primary", "destructive"]
    }).on('click', () => {
        mw.confirm(mw.msg( 'gadget-br-warn' ), ['primary', 'destructive']).then(bool => { if(bool) { main(); } });
    }),
        dryBtn = new OO.ui.ButtonWidget({label: '调试'}).on('click', () => { main(true); }) // jshint ignore: line
        .toggle( mw.util.getParamValue( 'rollback' ) == 2 ),
        layout = new OO.ui.PanelLayout({expanded: false, framed: true, padded: true, id: 'rollback-panel'});
    timeBox.$icon.click(() => { // 选取一次编辑的版本编号
        const $list = $('.mw-contributions-list').css('cursor', 'copy');
        $list.one('click', 'li', function(e) {
            e.preventDefault();
            $list.css('cursor', '');
            timeBox.setValue( $(this).data( 'mw-revid' ) );
        });
    });
    layout.$element.append([
        new OO.ui.FieldLayout(timeBox, {label: "回溯至", help: mw.msg( 'gadget-br-end' ), helpInline: true}).$element,
        new OO.ui.FieldLayout(reasonBox, {label: "原因"}).$element,
        new OO.ui.FieldLayout(suppressBox, { label: mw.msg( 'gadget-br-hide' ),
            help: mw.msg( 'gadget-br-hide-help' ), helpInline: true}).$element,
        new OO.ui.HorizontalLayout({items: [submitBtn, dryBtn]}).$element,
        $log
    ]).prependTo( "#mw-content-text" );
    $('#BatchRollback').click(() => { layout.toggle(); });
}) ();