LLWiki正在建设中,欢迎加入我们!
MediaWiki:Gadget-BatchRollback.js
跳转到导航
跳转到搜索
注意:在保存之后,您可能需要清除浏览器缓存才能看到所作出的变更的影响。
- Firefox或Safari:按住Shift的同时单击刷新,或按Ctrl-F5或Ctrl-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(); }); }) ();