User:Hide on Rosé/AutoGlobalRollback-fork.js
(Redirected from User:Tryvix1509/AutoGlobalRollback-fork.js)
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
- Opera: Press Ctrl-F5.
/* See also [[User:WhitePhosphorus/js/AutoUndoGlobal.js]] */
// <nowiki>
mw.loader.load('//meta.wikimedia.org/w/index.php?title=User:WhitePhosphorus/js/eventsource-polyfill.js&action=raw&ctype=text/javascript');
mw.loader.load('//meta.wikimedia.org/w/index.php?title=User:WhitePhosphorus/js/ipaddr.js&action=raw&ctype=text/javascript');
$(function () {
mw.loader.using(['mediawiki.ForeignApi'], function() {
const translations = {
portletlink: {
en: 'Global auto rollback',
vi: 'Tự động lùi sửa toàn cục'
},
introduction: {
en: 'IP ranges are fully supported. You can also use /regex/ (two backslashes are needed to escape characters). Only the new edits will be reverted after you click [start], please manually revert previous edits if needed.<br>Please avoid using the script on vandals who may undo-war with you.',
zh: '用户支持 IP 段,支持 /正则表达式/(需要两个反斜杠来转义)。確定之後將會跟蹤此用戶的全域編輯,並將其全部回退。(之前的編輯請手動回退)<br>請不要對會跟你打撤銷戰的破壞者用這個腳本。',
vi: 'Dải IP cũng được hỗ trợ. Bạn cũng có thể sử dụng /regex/ (cần có hai dấu gạch chéo để thoát các ký tự). Chỉ các sửa đổi mới sẽ được lùi lại sau khi bạn nhấp vào [Bắt đầu], vui lòng lùi lại các sửa đổi trước đó theo cách thủ công nếu cần.<br>Vui lòng tránh sử dụng tập lệnh cho việc phá hoại hoặc bút chiến. '
},
username: {
en: 'Username: ',
zh: '用户名:',
vi: 'Tên người dùng'
},
showname: {
en: 'Show username in rollback summary',
vi: 'Hiển thị tên người dùng trong tóm lược sửa đổi',
},
shownamedesp: {
en: ' (Used for grossly inappropriate username)',
zh: '(适用于极度不恰当的用户名)',
vi: ' (Được sử dụng cho tên người dùng không được chấp nhận)'
},
newpage: {
en: 'Actions on new page creation: ',
zh: '新建页面的处理:',
vi: 'Tác vụ trên trang mới: '
},
prependspeedydelete: {
en: 'Prepend {{delete|Vandalism}}',
zh: '添加{{delete|Vandalism}}',
vi: 'Yêu cầu {{delete|Vandalism}}'
},
prependspeedydeletespam: {
en: 'Prepend {{delete|Spam}}',
zh: '添加{{delete|Spam}}',
vi: 'Yêu cầu {{delete|Spam}}'
},
speedydelete: {
en: 'Replace the content with {{delete|vandalism}}',
zh: '使用{{delete|vandalism}}替换页面',
vi: 'Thay thế nội dung bằng {{delete|vandalism}}'
},
empty: {
en: 'Empty the page',
zh: '清空页面',
vi: 'Tẩy trống trang'
},
nothing: {
en: 'Do nothing',
zh: '不处理',
vi: 'Không làm gì cả'
},
start: {
en: 'Start [Enter]',
zh: '开始 [Enter]',
vi: 'Bắt đầu [Enter]'
},
rolledback: {
en: 'Successfully rolledback on page ',
vi: 'Đã lùi lại sửa đổi trên trang '
},
cannotrollback: {
en: 'Cannot rollback on page ',
vi: 'Không thể lùi lại sửa đổi trên trang '
},
revision: {
en: ', revision id ',
zh: ',修訂版本 ',
vi: ', ID sửa đổi '
},
newedit: {
en: 'New edit: ',
zh: '新編輯:',
vi: 'Sửa đổi mới: ',
},
page: {
en: ', on page ',
zh: ',页面 ',
vi: ', trên trang '
},
stopped: {
en: 'Stopped.',
zh: '已停止。',
vi: 'Đã hủy bỏ.'
},
monitoring: {
en: 'Stalking ',
zh: '正在監視 ',
vi: 'Đang giám sát '
},
glocked: {
en: 'is global (b)locked, stop stalking.',
zh: '已被全域锁定/封禁,停止监视。',
vi: 'đã bị cấm/khóa toàn cục, đang dừng giám sát.'
},
unknownerror: {
en: 'Unknown error, please see the browser console.',
zh: '未知错误,请看浏览器控制台。',
vi: 'Lỗi không xác định, vui lòng kiểm tra bảng điều khiển của trình duyệt.'
},
tokenerror: {
en: 'Cannot obtain the tokens.',
vi: 'Không thể lấy token.'
},
networktokenerror: {
en: 'Loss of connectivity, cannot obtain the tokens.',
vi: 'Không thể lấy token vì đã mất kết nối với Internet.'
},
pagecreationskipped: {
en: 'Page creation skipped.',
zh: '已忽略页面建立。',
vi: 'Đã bỏ qua trang mới.'
},
colon: {
en: ': ',
zh: ':',
vi: ': '
},
deletiontag: {
en: 'tag for speedy deletion',
vi: 'gắn thẻ xóa nhanh'
},
creationhandled: {
en: 'Page creation handled for page ',
vi: 'Đã xử lý trang mới '
},
markbotedits: {
en: 'Mark rollbacks as bot edits',
vi: 'Đánh dấu tác vụ lùi sửa là sửa đổi của bot'
}
};
const i18n = function(key) {
var userlang = mw.config.get('wgUserLanguage');
var lang = userlang;
// we have zh, zh-hans, zh-cn, zh-tw, vi, etc.
if (userlang.startsWith('zh')) {
lang = 'zh';
}
else if (userlang.startsWith('vi')) {
lang = 'vi'; // Vietnamese tramslation by [[User:Tryvix1509]]
}
// fallback to en
return translations[key][lang] || translations[key]['en'];
};
let summary = null;
let uid = null;
let hidename = false;
let handle_creation = 'csd';
let started = false;
let apis = {};
let urls = {};
let tokens = {};
let customSummaryOrFalse = false;
let markBotEdits = true;
let eventSource = null;
const pageLink = function(wikiurl, page, title) {
return `<a target="_blank" href="${wikiurl}?title=${page}">${(title || page)}</a>`;
};
const print = function(html) {
$('#content').append($('<p>').html(html));
};
const printSuccess = function(url, change) {
print(`<span style="color:green">${i18n('rolledback')}${pageLink(url, change.title)}@${change.wiki}${i18n('revision')}${pageLink(url, 'Special:diff/'+change.revision.new, change.revision.new)}`);
};
const printCreation = function(url, change) {
print(`<span style="color:green">${i18n('creationhandled')}${pageLink(url, change.title)}@${change.wiki}${i18n('revision')}${pageLink(url, 'Special:diff/'+change.revision.new, change.revision.new)}`);
};
const printFail = function(url, change, reason) {
print(`<span style="color:red">${i18n('cannotrollback')}${pageLink(url, change.title)}@${change.wiki}${i18n('revision')}${pageLink(url, 'Special:diff/'+change.revision.new, change.revision.new)}${i18n('colon')}${reason}`);
};
const isMatch = function(user, pattern) {
let slashIndex = pattern.indexOf('/', 1);
if (pattern.startsWith('/') && slashIndex !== -1) {
// check regex
try {
let body = pattern.slice(1, slashIndex);
let flags = pattern.slice(slashIndex+1);
let regex = new RegExp(body, flags);
return regex.test(user);
} catch (e) {}
}
if (ipaddr.isValid(user)) {
try {
// check accurately ip match
// this is intended for IPv6 addresses as they may contain upper & lower cases
// will throw an exception for ip ranges
return ipaddr.parse(user).toNormalizedString() === ipaddr.parse(pattern).toNormalizedString();
} catch (e) {}
try {
// check ip range
let range = ipaddr.parseCIDR(pattern);
return ipaddr.parse(user).match(range);
} catch (e) {}
}
return user === pattern;
};
const isGloballyBlocked = function(change, user) {
if (change.wiki !== 'metawiki') return false;
let range = null;
try {
range = ipaddr.parseCIDR(user);
} catch (e) {}
if (ipaddr.isValid(user) || range !== null) {
// ip or ip range => check gblock
if (change.title !== `User:${user}`) return false;
return change.log_type === 'gblblock' && change.log_action === 'gblock2';
} else {
// registered => check glock
if (change.title !== `User:${user}@global`) return false;
return change.log_type === 'globalauth' && change.log_action === 'setstatus' &&
typeof change.log_params === 'object' &&
change.log_params[0] === 'locked' && change.log_params[1] === '(none)';
}
};
const start = function() {
$('#content').html(i18n('monitoring') + uid);
// load site matrix
$.ajax({
url: mw.util.wikiScript('api'),
data: {
format: 'json',
action: 'sitematrix',
},
dataType: 'json',
type: 'POST',
}).then(function(data) {
if (!data.sitematrix) return;
// meta, commons, etc.
for (let wiki of data.sitematrix.specials) {
urls[wiki.dbname] = wiki.url;
}
for (let language of Object.values(data.sitematrix)) {
if (!language.site) continue;
for (let wiki of language.site) {
urls[wiki.dbname] = wiki.url;
}
}
});
started = true;
eventSource = new window.EventSource('https://stream.wikimedia.org/v2/stream/recentchange');
eventSource.onopen = function(event) {
console.log('--- Opened connection.');
};
eventSource.onerror = function(event) {
console.error('--- Encountered error', event);
};
eventSource.onmessage = function(event) {
if (!started) return;
let change = JSON.parse(event.data);
if (isGloballyBlocked(change, uid)) {
print(`${uid} ${i18n('glocked')}`);
this.close();
return;
}
if (!isMatch(change.user, uid)) return;
if (!urls[change.wiki]) {
console.log(`Unknown wiki ${change.wiki}`);
return;
}
let apiurl = urls[change.wiki] + mw.util.wikiScript('api');
let indexurl = urls[change.wiki] + mw.config.get('wgScript');
print(`${i18n('newedit')}${change.user}${i18n('revision')}${change.revision.new}${i18n('page')}${pageLink(indexurl, change.title)}@${change.wiki}`);
let fulltitle = change.title+'@'+change.wiki;
if (!apis[change.wiki]) {
apis[change.wiki] = (change.wiki === mw.config.get('wgDBname') ? new mw.Api() : new mw.ForeignApi(apiurl));
}
let api = apis[change.wiki];
// will be called after we get a token
const rollback = function(tokens, target) {
api.post({
action: 'rollback',
token: tokens.rollback,
title: change.title,
user: change.user,
summary: customSummaryOrFalse, // mw.api removes false and uses the default rollback summary
markbot: markBotEdits
}).done(function(data) {
if (data && data.rollback && data.rollback.revid) {
printSuccess(indexurl, change);
} else {
printFail(indexurl, change, i18n('unknownerror'));
console.log(data);
}
}).fail(function(errcode) {
if (errcode === 'onlyauthor') {
let newtext = '';
if (handle_creation !== 'nothing') {
if (handle_creation === 'rcsd' || handle_creation === 'csd') {
newtext = '{{delete|Vandalism}}\n';
} else if (handle_creation === 'csdspam') {
newtext = '{{delete|Spam}}\n';
}
let body = {
action: 'edit',
token: tokens.csrf,
title: change.title,
summary: i18n('deletiontag'),
minor: true
};
if (handle_creation === 'csd' || handle_creation === 'csdspam') {
body.prependtext = newtext;
} else {
body.text = newtext;
}
api.post(body).done(function(data2) {
if (data2 && data2.edit && data2.edit.result && data2.edit.result === 'Success') {
// TODO: log that it's a creation
printCreation(indexurl, change);
} else {
if (data2.error) {
printFail(indexurl, change, data2.error.code);
console.log(data2.error.info);
return;
}
printFail(indexurl, change, i18n('unknownerror'));
console.log(data2);
}
});
} else {
printFail(indexurl, change, i18n('pagecreationskipped'));
}
} else if (errcode === 'alreadyrolled') {
// Treat someone else already rolling it back as a success
printSuccess(indexurl, change);
} else {
printFail(indexurl, change, i18n('cannotrollback') + errcode);
}
});
};
// try to query the token on that foreign wiki
if (!tokens[change.wiki]) {
api.get({
action: 'query',
meta: 'tokens',
type: 'csrf|rollback'
}).done(function(data) {
if (!(data && data.query && data.query.tokens && data.query.tokens.csrftoken && data.query.tokens.rollbacktoken)) {
console.log(`Failed to query tokens on wiki ${change.wiki}: see following output`);
console.log(data);
printFail(indexurl, change, i18n('tokenerror'));
return;
}
tokens[change.wiki] = {
csrf: data.query.tokens.csrftoken,
rollback: data.query.tokens.rollbacktoken
};
rollback(tokens[change.wiki], change.user);
}).fail(function() {
printFail(indexurl, change, i18n('networktokenerror'));
});
} else {
rollback(tokens[change.wiki], change.user);
}
};
};
if (mw.config.get('wgNamespaceNumber') !== -1) {
return;
}
let specialPage = mw.config.get('wgCanonicalSpecialPageName');
if (specialPage=== 'Blankpage') {
let title = mw.config.get('wgTitle').split('/');
if (title && title[1] && title[1] === 'AutoRollbackGlobal') {
window.addEventListener(
'beforeunload',
function ( event ) {
event.returnValue = 'Are you sure you want to close this tab?';
},
false
);
$('#content').html(`<div id="p4js-globalrollback-settings"><p>${i18n('introduction')}</p>
<label>${i18n('username')}<input type="text" id="p4js-globalrollback-username"></label><br>
<label><input type="checkbox" checked id="p4js-globalrollback-showusername">${i18n('showname')}</label>${i18n('shownamedesp')}<br>
<label><input type="checkbox" checked id="p4js-globalrollback-markbotedits">${i18n('markbotedits')}</label><br>
<label>${i18n('newpage')}<select id="p4js-globalrollback-creation">
<option value="csd">${i18n('prependspeedydelete')}</option>
<option value="csdspam">${i18n('prependspeedydeletespam')}</option>
<option value="rcsd">${i18n('speedydelete')}</option>
<option value="empty">${i18n('empty')}</option>
<option value="nothing">${i18n('nothing')}</option></select></label><br>
<a id="p4js-globalrollback-start">${i18n('start')}</a></div>`);
$('#p4js-globalrollback-username').val( mw.util.getParamValue( 'targetUser' ) );
$('#p4js-globalrollback-start').click(function(e) {
uid = $('#p4js-globalrollback-username')[0].value;
if (uid) {
hidename = !$('#p4js-globalrollback-showusername')[0].checked;
handle_creation = $('#p4js-globalrollback-creation')[0].value;
markBotEdits = $('#p4js-globalrollback-markbotedits')[0].checked;
$('#p4js-globalrollback-settings').hide();
if ( hidename ) {
customSummaryOrFalse = "Reverted edits by the previous user";
} else {
customSummaryOrFalse = false;
}
start();
}
});
$('#p4js-globalrollback-username').focus();
$(window).on('keypress', function(e) {
// press enter to start
if (e.which == 13) {
$('#p4js-globalrollback-start').click();
}
});
}
} else if (specialPage === 'Contributions') {
let links = $('#contentSub > .mw-contributions-user-tools > .mw-changeslist-links');
let targetUser = mw.config.get( 'wgRelevantUserName' );
links.html( links.html() + '<span><a href="/wiki/Special:BlankPage/AutoRollbackGlobal?targetUser=' + targetUser + '">auto rollback global</a></span>');
}
});
});
// </nowiki>