User:DannyS712/AutoRollbackGlobal 2.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.
// <nowiki>
// Quick script to automatically handle edits globally, including optionally rolling back edits and
// adding deletion tags
//
// See also [[User:WhitePhosphorus/js/AutoUndoGlobal.js]] and [[User:DannyS712/AutoRollbackGlobal.js]]
// @author DannyS712
/* jshint maxerr: 999 */
$(() => {
const AutoRollbackGlobal = {};
window.AutoRollbackGlobal = AutoRollbackGlobal;
AutoRollbackGlobal.init = function () {
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');
window.document.title = 'AutoRollbackGlobal';
$( '#firstHeading' ).text( 'AutoRollbackGlobal' );
mw.loader.using(
[ 'mediawiki.api', 'mediawiki.ForeignApi', 'mediawiki.util', 'oojs-ui-core', 'oojs-ui-widgets', 'oojs-ui-windows' ],
AutoRollbackGlobal.run
);
window.addEventListener(
'beforeunload',
function ( event ) {
event.returnValue = 'Are you sure you want to close this tab?';
},
false
);
};
AutoRollbackGlobal.elements = {};
AutoRollbackGlobal.run = function () {
mw.util.addCSS( `
.AutoRollbackGlobal-bad {
color: #F00;
}
.AutoRollbackGlobal-good {
color: #00D100;
}
` );
var e = {};
e.targetUser = new OO.ui.TextInputWidget( {
value: mw.util.getParamValue( 'targetUser' )
} );
e.targetUserLayout = new OO.ui.FieldLayout( e.targetUser, { label: 'User' } );
e.doRollback = new OO.ui.CheckboxInputWidget();
e.doRollbackLayout = new OO.ui.FieldLayout( e.doRollback, { label: 'Try to rollback new edits' } );
e.botRollback = new OO.ui.CheckboxInputWidget();
e.botRollbackLayout = new OO.ui.FieldLayout( e.botRollback, { label: 'Mark rollbacks as bot edits' } );
e.hideUsername = new OO.ui.CheckboxInputWidget();
e.hideUsernameLayout = new OO.ui.FieldLayout( e.hideUsername, { label: 'Hide username in rollback summary (Used for grossly inappropriate username)' } );
e.newPageAction = new OO.ui.RadioSelectInputWidget( {
options: [
{ data: 'nothing', label: 'Do nothing' },
{ data: 'prependVandalism', label: 'Prepend {{delete|Vandalism}}' }
]
} );
e.newPageActionLayout = new OO.ui.FieldLayout( e.newPageAction, { label: 'Action on new page creations' } );
e.audioNotification = new OO.ui.CheckboxInputWidget();
e.audioNotificationLayout = new OO.ui.FieldLayout( e.audioNotification, { label: 'Play a sound when anything happens (edit, page creation, or lock/block)' } );
var submit = new OO.ui.ButtonInputWidget( {
label: 'Start [enter]',
flags: [
'primary',
'destructive'
]
} );
submit.on( 'click', function () {
AutoRollbackGlobal.onSubmit();
} );
$( window ).on( 'keypress', function ( e ) {
// press enter to start
if ( e.which == 13 ) {
submit.simulateLabelClick();
}
} );
var fieldSet = new OO.ui.FieldsetLayout( {
label: 'IP ranges are fully supported. You can also use /regex/ (two backslashes are needed to escape characters).' +
' Only the new edits will be shown after you click [start]'
} );
fieldSet.addItems( [
e.targetUserLayout,
e.doRollbackLayout,
e.botRollbackLayout,
e.hideUsernameLayout,
e.newPageActionLayout,
e.audioNotificationLayout,
new OO.ui.FieldLayout( submit )
] );
var $results = $('<div>')
.attr( 'id', 'autorollbackglobal-results' );
$('#mw-content-text').empty().append(
$( '<span>' ).append(
'Source code: ',
$( '<a>' ).attr( 'href', 'https://meta.wikimedia.org/wiki/User:DannyS712/AutoRollbackGlobal 2.js' ).text( '[[:m:User:DannyS712/AutoRollbackGlobal 2.js]]' )
),
fieldSet.$element,
$( '<hr>' ),
$results
);
AutoRollbackGlobal.elements = e;
};
AutoRollbackGlobal.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)';
}
};
AutoRollbackGlobal.onErrHandler = function () {
// Shared error handler
alert( 'Something went wrong' );
console.log( arguments );
};
AutoRollbackGlobal.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;
};
AutoRollbackGlobal.pageLink = function(wikiurl, page, title) {
return `<a target="_blank" href="${wikiurl}?title=${page}">${(title || page)}</a>`;
};
AutoRollbackGlobal.addResult = function ( resultText, resultType ) {
var $res = $( '<p>' ).html( resultText );
resultType = resultType || '';
if ( resultType === 'good' || resultType === 'bad' ) {
$res.addClass( 'AutoRollbackGlobal-' + resultType );
}
$( '#autorollbackglobal-results' ).append( $res );
};
AutoRollbackGlobal.doWikiRollback = function ( wikiApi, rollbackToken, changeToRevert, customSummaryOrFalse, markBotEdits, indexUrl ) {
wikiApi.post( {
action: 'rollback',
assert: 'user',
title: changeToRevert.title,
user: changeToRevert.user,
summary: customSummaryOrFalse,
markbot: markBotEdits,
token: rollbackToken
} ).done(
function ( data ) {
if ( data && data.rollback && data.rollback.revid ) {
AutoRollbackGlobal.addResult(
'Successfully rolledback on page ' +
AutoRollbackGlobal.pageLink( indexUrl, changeToRevert.title ) +
'@' + changeToRevert.wiki,
'good'
);
} else {
AutoRollbackGlobal.addResult(
'Cannot rollback on page ' +
AutoRollbackGlobal.pageLink( indexUrl, changeToRevert.title ) +
'@' + changeToRevert.wiki + ' - more info in the console'
,
'bad'
);
console.log( data );
}
}
).fail(
function ( errcode ) {
AutoRollbackGlobal.addResult(
'Cannot rollback on page ' +
AutoRollbackGlobal.pageLink( indexUrl, changeToRevert.title ) +
'@' + changeToRevert.wiki +
' - error code: `' + errcode + '` - more info in the console'
,
'bad'
);
console.log( errcode );
}
);
};
AutoRollbackGlobal.doWikiNewPage = function ( wikiApi, csrfToken, changeToHandle, wayToHandle, indexUrl ) {
// wayToHandle is what the user chose. Only reached if not `nothing`. Supported options:
// `prependVandalism`
var summaries = {
prependVandalism: 'Prepending deletion tag'
};
var requestBody = {
action: 'edit',
assert: 'user',
title: changeToHandle.title,
summary: summaries[ wayToHandle ]
};
// hanlde specific choice
if ( wayToHandle === 'prependVandalism' ) {
requestBody.prependtext = '{{delete|Vandalism}}\n';
}
requestBody.token = csrfToken;
wikiApi.post( requestBody ).done(
function ( data ) {
if ( data && data.edit && data.edit.result && data.edit.result === 'Success' ) {
AutoRollbackGlobal.addResult(
'Successfully handled new page creation for ' +
AutoRollbackGlobal.pageLink( indexUrl, changeToHandle.title ) +
'@' + changeToHandle.wiki,
'good'
);
} else {
AutoRollbackGlobal.addResult(
'Error in handling new page creation for ' +
AutoRollbackGlobal.pageLink( indexUrl, changeToHandle.title ) +
'@' + changeToHandle.wiki + ' - more info in the console'
,
'bad'
);
console.log( data );
}
}
).fail(
function ( errcode ) {
AutoRollbackGlobal.addResult(
'Error in handling new page creation for ' +
AutoRollbackGlobal.pageLink( indexUrl, changeToHandle.title ) +
'@' + changeToHandle.wiki +
' - error code: `' + errcode + '` - more info in the console'
,
'bad'
);
console.log( data );
}
);
};
AutoRollbackGlobal.handleChange = function ( wikiApi, tokens, changeToHandle, params, indexUrl ) {
if ( changeToHandle.type === 'edit' ) {
if ( params.doRollback === true ) {
AutoRollbackGlobal.doWikiRollback(
wikiApi,
tokens.rollback,
changeToHandle,
params.rollbackSummary,
params.botRollback,
indexUrl
);
} else {
AutoRollbackGlobal.addResult(
'Skipping edit at ' +
AutoRollbackGlobal.pageLink( indexUrl, changeToHandle.title ) +
'@' + changeToHandle.wiki
,
'bad'
);
}
} else if ( changeToHandle.type === 'new' ) {
if ( params.newPageAction !== 'nothing' ) {
AutoRollbackGlobal.doWikiNewPage(
wikiApi,
tokens.csrf,
changeToHandle,
params.newPageAction,
indexUrl
);
} else {
AutoRollbackGlobal.addResult(
'Skipping new page creation for ' +
AutoRollbackGlobal.pageLink( indexUrl, changeToHandle.title ) +
'@' + changeToHandle.wiki
,
'bad'
);
}
}
};
AutoRollbackGlobal.onSubmit = function () {
var e = AutoRollbackGlobal.elements;
var params = {
targetUser: e.targetUser.getValue(),
doRollback: e.doRollback.isSelected(),
botRollback: e.botRollback.isSelected(),
newPageAction: e.newPageAction.getValue(),
audioNotification: e.audioNotification.isSelected()
};
// `false` will be removed by mw.Api and thus the default will be used
params.rollbackSummary = e.hideUsername.isSelected() ? 'Reverted edits by the previous user' : false;
console.log( 'Form submitted with: ', params );
var targetUser = params.targetUser;
$( '#autorollbackglobal-results' ).empty();
var $audioNotification = false;
if ( params.audioNotification ) {
$audioNotification = $( '<audio>' )
.attr( 'id', 'autorollbackglobal-audio' )
// [[:commons:File:Notification (Gravity Sound).wav]]
.attr( 'src', 'https://upload.wikimedia.org/wikipedia/commons/1/10/Notification_%28Gravity_Sound%29.wav' );
$( '#autorollbackglobal-results' ).append( $audioNotification );
}
function maybePlayNotification() {
if ( $audioNotification ) {
$audioNotification.trigger( 'play' );
}
}
AutoRollbackGlobal.addResult( 'Handling new edits for: ' + targetUser );
var urls = {};
var apisByWiki = {};
var tokensByWiki = {};
new mw.Api().get( {
action: 'sitematrix',
} ).then( function ( data ) {
if ( !data.sitematrix ) {
console.log( data );
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;
}
}
});
var 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) {
let change = JSON.parse(event.data);
if ( AutoRollbackGlobal.isGloballyBlocked( change, targetUser ) ) {
AutoRollbackGlobal.addResult( targetUser + ' is global (b)locked, stop updating live edits.' );
maybePlayNotification();
this.close();
return;
}
if ( !AutoRollbackGlobal.isMatch( change.user, targetUser ) ) {
return;
}
if ( !urls[change.wiki] ) {
console.log(`Unknown wiki ${change.wiki}`);
return;
}
console.log( change );
if ( change.type !== 'edit' && change.type !== 'new' ) {
return;
}
let indexUrl = urls[change.wiki] + mw.config.get('wgScript');
var changeType = ( change.type === 'edit' ? 'New edit' : 'New page' );
var line = changeType + ': revision id ' +
AutoRollbackGlobal.pageLink( indexUrl, 'Special:Diff/' + change.revision.new, change.revision.new ) +
', on page ' +
AutoRollbackGlobal.pageLink( indexUrl, change.title ) +
'@' +
change.wiki;
AutoRollbackGlobal.addResult( line );
maybePlayNotification();
if ( !apisByWiki[change.wiki] ) {
let apiurl = urls[change.wiki] + mw.util.wikiScript('api');
apisByWiki[change.wiki] = (change.wiki === mw.config.get('wgDBname') ? new mw.Api() : new mw.ForeignApi( apiurl ) );
}
let targetWikiApi = apisByWiki[change.wiki];
// try to query the token on that foreign wiki
if ( !tokensByWiki[change.wiki] ) {
targetWikiApi.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);
AutoRollbackGlobal.addResult( 'Could not get tokens for wiki: ' + change.wiki, 'bad' );
return;
}
tokensByWiki[change.wiki] = {
csrf: data.query.tokens.csrftoken,
rollback: data.query.tokens.rollbacktoken
};
AutoRollbackGlobal.handleChange( targetWikiApi, tokensByWiki[change.wiki], change, params, indexUrl );
} ).fail( function( errCode ) {
AutoRollbackGlobal.addResult(
'Could not get tokens for wiki: ' + change.wiki + ' - error code `' + errCode + '`',
'bad'
);
});
} else {
AutoRollbackGlobal.handleChange( targetWikiApi, tokensByWiki[change.wiki], change, params, indexUrl );
}
};
};
});
$(document).ready(() => {
if ( mw.config.get( 'wgNamespaceNumber' ) === -1 ) {
const page = mw.config.get( 'wgCanonicalSpecialPageName' );
if ( page === 'Blankpage' ) {
const page2 = mw.config.get( 'wgTitle' ).split( '/' );
if ( page2[1] && page2[1] === 'AutoRollbackGlobal' ) {
window.AutoRollbackGlobal.init();
}
} else if ( page === '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>