User:Deon/recent2.js

/*

This tool hits the RSS feed for recent changes every 30 seconds or

so and checks for common vandalism. It does not make a separate

server request for every edit.

  • /

//

recent2={

// Edit these to your liking.

// Make sure there's a comma at the end of each line.

badwordsUrl: 'User:Lupin/badwords',

filterPage: 'User:Lupin/Filter_recent_changes',

allRecentPage: 'User:Lupin/All_recent_changes',

recentIPPage: 'User:Lupin/Recent_IP_edits',

monitorWatchlistPage: 'User:Lupin/Monitor_my_watchlist',

spelldictUrl: 'Wikipedia:Lists_of_common_misspellings/For_machines',

spelldictPage: 'User:Lupin/Live_spellcheck',

safePages: '([Ww]ikipedia:([Ii]ntroduction|[Ss]andbox|[Tt]utorial[^/]*/sandbox)|[Tt]emplate:(X[1-9]|Template sandbox))',

linkify: true,

updateSeconds: 30,

ipUserRegex: RegExp('(User:)?((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\\.){3}' +

'(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])'),

outputSeparator: '


',

templateNamespace: 'Template',

namespaces: { 'Media':1, "Special":1, "User":1, "User talk":1, "Wikipedia":1,

"Wikipedia talk":1, "Image":1, "Image talk":1, "MediaWiki":1,

"MediaWiki talk":1, "Template":1, "Template talk":1, "Help":1,

"Help talk":1, "Category":1, "Category talk":1, "Portal":1, "Portal talk":1 },

apiAulimitUser: 500,

apiAulimitSysop: 5000,

backgroundWindowsMax: 10,

dummy: null // leave this last one alone

};

recent2.download=function(bundle) {

// mandatory: bundle.url

// optional: bundle.onSuccess (xmlhttprequest, bundle)

// optional: bundle.onFailure (xmlhttprequest, bundle)

// optional: bundle.otherStuff OK too, passed to onSuccess and onFailure

var x = window.XMLHttpRequest ? new XMLHttpRequest()

: window.ActiveXObject ? new ActiveXObject("Microsoft.XMLHTTP")

: false;

if (x) {

x.onreadystatechange=function() {

x.readyState==4 && recent2.downloadComplete(x,bundle);

};

x.open("GET",bundle.url,true);

x.send(null);

}

return x;

}

recent2.downloadComplete=function(x,bundle) {

x.status==200 && ( bundle.onSuccess && bundle.onSuccess(x,bundle) || true )

|| ( bundle.onFailure && bundle.onFailure(x,bundle) || alert(x.statusText));

};

if (! recent2.outputPosition) { recent2.outputPosition=''; }

window.gettingBadWords=false;

window.badWords=null;

// paths

if ( typeof(wgServer)!='string' ||

typeof(wgArticlePath)!='string' ||

typeof(wgScriptPath)!='string') {

recent2.articlePath= 'http://' + document.location.hostname + '/wiki/';

recent2.scriptPath= 'http://' + document.location.hostname + '/w/';

} else {

recent2.articlePath=mw.config.get('wgServer')+mw.config.get('wgArticlePath').replace(/\$1/, '');

recent2.scriptPath=mw.config.get('wgServer')+mw.config.get('wgScriptPath')+'/';

}

recent2.getBadWords=function() {

window.gettingBadWords=true;

recent2.download({ url: recent2.scriptPath + 'index.php?title=' +

recent2.badwordsUrl + '&action=raw&ctype=text/css&max-age=7200', // reload every 2 h

onSuccess: recent2.processBadWords,

onFailure: function () { setTimeout(recent2.getBadWords, 15000); return true;}});

}

window.diffCellRe=RegExp("\\+<\\/td>\\s*" +

"]*>\\s*

\\s*(.*?)\\s*<\\/div>\\s*<\\/td>", 'gi');

// processBadWords: generate the badWords RegExp from

// the downloaded data.

// d is the xmlhttprequest object from the download

recent2.processBadWords=function(d) {

var data=d.responseText.split('\n');

var phrase=[];

var string=[];

for (var i=0; i

var s=data[i];

// ignore empty lines, whitespace-only lines and lines starting with '<'

if (/^\s*$|^

// lines beginning and ending with a (back-)slash (and possibly trailing

// whitespace) are treated as regexps

if (/^([\\\/]).*\1\s*$/.test(s)) {

var isPhrase=(s.charAt(0)=='/');

// remove slashes and trailing whitespace

s=s.replace(/^([\\\/])|([\\\/]\s*$)/g, '');

// escape opening parens: ( -> (?:

s=s.replace(/\(?!\?/g, '(?:');

// check that s represents a valid regexp

try { var r=new RegExp(s); }

catch (err) {

var errDiv=newOutputDiv('recent2_error', recent2.outputPosition);

errDiv.innerHTML='Warning: ignoring odd-looking regexp on line '+i

+' of badwords:

' + s + '
';

continue;

}

if (isPhrase) phrase.push(s); else string.push(s);

} else {

// treat this line as a non-regexp and escape it.

phrase.push(s.replace(RegExp('([-|.()\\+:!,?*^${}\\[\\]])', 'g'), '\\$1'));

}

}

// 123 3 2|4 4|5 56 67 71

// ((( repeated char ) )|( ... | strings | ... )|( border )( ... | phrases | ... )( border ))

window.badWords=RegExp("((([^\\-\\|\\{\\}\\].\\s'=wI:*#0-9a-f])\\3{2,})|(" + string.join('|') + ")|(^|[^/\\w])(" + phrase.join('|') + ")(?![/\\w]))", 'gi');

};

window.gettingWatchlist=false;

recent2.watchlist=null;

recent2.getWatchlist=function() {

window.gettingWatchlist=true;

recent2.download({url: recent2.articlePath + 'Special:Watchlist/raw',

onSuccess: recent2.processWatchlist,

onFailure: function () { setTimeout(getWatchlist, 15000); return true; }});

};

recent2.processWatchlist=function(req, bundle) {

var watchlist={};

var lines=req.responseText.split('\n');

var inList=false;

var article = '';

for (var i=0; i < lines.length; ++i) {

if (inList || lines[i].indexOf('') == 0) {

window.watchlist = watchlist;

return;

}

if (!inList) {

inList = true;

article = lines[i].replace (/^.*>/, '');

} else {

article=lines[i];

}

watchlist[article] = true;

}

}

};

window.gettingSpelldict=false;

window.spelldict=null;

recent2.getSpelldict=function() {

window.gettingSpelldict=true;

recent2.download({url: recent2.scriptPath + 'index.php?title=' + recent2.spelldictUrl + '&action=raw&ctype=text/css',

onSuccess: recent2.processSpelldict,

onFailure: function () { setTimeout(getSpelldict, 15000); return true; }});

};

recent2.processSpelldict=function(req, bundle) {

var spelldict={};

var lines=req.responseText.split('\n');

var a=[];

for (var i=0; i

var split=lines[i].split('->');

if (split.length<2) { continue; }

split[1]=split.slice(1).join('->').split(/, */);

split[0]=split[0].toLowerCase().replace(/^\s*/, '');

spelldict[split[0]]=split[1];

a.push(split[0]);

}

window.spelldict=spelldict;

window.spellRe=RegExp('\\b(' + a.join('|') + ')\\b', 'i');

};

recent2.feed=recent2.scriptPath + 'index.php?title=Special:Recentchanges&feed=rss&action=purge';

window.newOutputDiv=function(klass, position, immortal) {

var h1=document.getElementsByTagName('h1')[0];

var ret=document.createElement('div');

if (klass) { ret.className=klass; }

if (!position) { position='bottom'; }

switch(position) {

case 'top':

h1.parentNode.insertBefore(ret, h1.nextSibling);

break;

case 'bottom':

h1.parentNode.appendChild(ret);

break;

default:

if (!newOutputDiv.alerted) {

alert('Unknown position '+position+' in recent2.js, newOutputDiv');

window.newOutputDiv.alerted=true;

}

return newOutputDiv(klass, 'bottom');

}

if (!immortal) { ret.id=newOutputDiv.uid++; }

window.outputDivs.push(ret);

return ret;

};

window.newOutputDiv.alerted=false;

window.newOutputDiv.uid=0;

window.outputDivs=[];

window.grabRecentChanges=function(feed) {

if (! window.badWords && recent2.filter_badwords ) {

if ( ! window.gettingBadWords ) { recent2.getBadWords(); }

return setTimeout(function(){grabRecentChanges(feed);}, 500);

}

if (! window.watchlist && recent2.filter_watchlist) {

if (! window.gettingWatchlist ) recent2.getWatchlist();

return setTimeout(function(){grabRecentChanges(feed);}, 500);

}

if (! window.spelldict && recent2.filter_spelling) {

if (! window.gettingSpelldict) recent2.getSpelldict();

return setTimeout(function(){grabRecentChanges(feed);}, 500);

}

if (typeof(recent2.sysopRegExp) == 'undefined') {

if (! recent2.gettingSysops) recent2.getSysops();

return setTimeout(function(){grabRecentChanges(feed);}, 500);

}

var pos=recent2.outputPosition;

if (pos=='top') {

var output=newOutputDiv('recent2.lines', pos);

var status=newOutputDiv('recent2.status', pos);

} else {

var status=newOutputDiv('recent2.status', pos);

var output=newOutputDiv('recent2.lines', pos);

}

status.style.borderStyle='solid';

status.style.borderColor='orange';

status.innerHTML=greyFont+'(' + recent2.count + ') updating...';

// this abort stuff doesn't work properly for some reason...

//recent2.lastFeedDownload && recent2.lastFeedDownload.abort(); // } catch (summatNasty) { /* do nothing */ }

recent2.lastFeedDownload=recent2.download({url: feed,

onSuccess: processRecentChanges,

output: output, status: status, onFailure: feedFailed});

};

var greyFont='';

window.feedFailed=function(x,bundle) {

try { bundle.status.innerHTML+=greyFont+'failed: '+x.statusText + ''; }

catch (err) { bundle.status.innerHTML+=greyFont+'failed badly: '+err+''; }

return true;

};

recent2.newWindows=true;

window.linkmaker=function(url, text) {

var s='

recent2.newWindows && (s += ' target="_blank"');

s += '>' + text + '';

return s;

};

recent2.delayedLines={};

recent2.delay=0;

window.processRecentChanges=function(req, bundle){

recent2.initialId=processRecentChanges.id;

recent2.latest=processRecentChanges.lastDate;

var doc;

if (doc=req.responseXML.documentElement) {

if (recent2.items=doc.getElementsByTagName('item')) {

if ((recent2.itemsCurrent=recent2.items.length) > 0) {

recent2.bundleRef = bundle;

processRecentChangesSingle(); // start processing one diff every 50 ms

return;

}

}

}

processRecentChangesDisplay(bundle);

return;

}

recent2.safePagesRe=new RegExp('^' + recent2.safePages + '$');

recent2.changeDelay=50; // delay between processing each diff, in ms

window.nextChangeSoon=function(rightNow) {

setTimeout(processRecentChangesSingle, rightNow ? 0 : recent2.changeDelay);

};

// process single diff items delayed by a short timespan

window.processRecentChangesSingle=function(){

recent2.itemsCurrent--;

var i = recent2.itemsCurrent;

var items = recent2.items;

if (i < 0) { processRecentChangesDisplay(recent2.bundleRef); return; }

var timestamp = Date.parse(getFirstTagContent(items[i],'pubDate'));

if (timestamp <= processRecentChanges.lastDate) { nextChangeSoon(true); return; }

recent2.latest = (timestamp > recent2.latest) ? timestamp : recent2.latest;

var diffText=getFirstTagContent(items[i],'description').split('').join('\n');

var editSummary=diffText.replace(RegExp('^

(.*?)

[\\s\\S]*'), '$1');

var editor=getFirstTagContent(items[i], 'creator') || getFirstTagContent(items[i], 'dc:creator');

if (recent2.ignore_my_edits && wgUserName==editor) { return; }

// NB article is the link attribute - a fully qualified URL

// strip out the &diff=...&oldid=... bit to leave only ?title=...

var article=getFirstTagContent(items[i], 'link').split('&')[0];

if (recent2.delayedLines[article] && recent2.delayedLines[article].editor != editor) {

delete recent2.delayedLines[article];

}

if (recent2.filter_anonsOnly && !recent2.ipUserRegex.test(editor)) { nextChangeSoon(true); return; }

// articleTitle is the wgTitle thingy with spaces and all that

var articleTitle=getFirstTagContent(items[i], 'title');

//console.info('articleTitle=%s', articleTitle);

if (recent2.ignore_safe_pages && recent2.safePagesRe.test(articleTitle)) {

//console.warn('Ignoring safe page %s', article);

nextChangeSoon(true); return;

}

if (recent2.hideNonArticles) {

var namespace=articleTitle.replace(/:.*/, '');

if (recent2.namespaces[namespace] &&

( ( recent2.showTemplates && namespace != recent2.templateNamespace ) ||

! recent2.showTemplates )) {

nextChangeSoon(true); return;

}

}

// perhaps ignore talk pages

if (! recent2.show_talkpages && articleTitle

&& /^Talk:|^[^:]*?[_ ]talk:/.test(articleTitle)) {

nextChangeSoon(true); return;

}

// perhaps restrict to watchlist articles

if (recent2.filter_watchlist && articleTitle &&

! window.watchlist[articleTitle.replace(/^Talk:/, '').replace(/[ _]talk:/, ':')]) {

nextChangeSoon(true); return;

}

// filter against badwords regexp

if (recent2.filter_badwords) {

var badMatch=null;

var diffCell=null;

var previousVandal= window.vandals[editor];

var matchesRe='';

var matchesPlain='';

diffCellRe.lastIndex=0;

while (diffCell=diffCellRe.exec(diffText)) {

// get content of addition table cells, faster than direct fulltext search

badWords.lastIndex=0;

// .test() is meant to be faster than a full match

if (badMatch=badWords.test(diffCell[1])) { break; }

}

if (badMatch===true || previousVandal) {

badWords.lastIndex=0;

var reMatch;

while (diffCell && (reMatch=badWords.exec(diffCell[1]))) {

var badWord=reMatch[2] || reMatch[4] || reMatch[6] || '';

if (articleTitle.toLowerCase().indexOf(badWord.toLowerCase())<0) { // avoid legit article title occurrences

badWord=badWord.replace(/^\s+|\s+$/g, '');

if (badWord!='') {

matchesPlain+=badWord+', ';

badWord=badWord.replace(/([^\w ])/g, '\\$1');

matchesRe+=badWord+'|';

}

}

}

matchesRe=matchesRe.replace(/\|$/, '');

matchesPlain=matchesPlain.replace(/, $/, '');

if (!previousVandal && matchesRe=='') { nextChangeSoon(); return; }

// highlighting

var highlighted=diffCell && diffCell[1];

if (matchesRe) {

highlighted=highlighted.replace(RegExp('('+matchesRe+')', 'g'),

'$1');

}

articleTitle=getFirstTagContent(items[i], 'title');

// linkify

highlighted=recent2.doLinkify(highlighted);

diffText=recent2.doLinkify(diffText);

if (previousVandal) {

matchesPlain = '[Previously rolled back this editor] ' + matchesPlain;

}

recent2.delayedLines[article]={

timestamp: timestamp, article:article, count:recent2.count, articleTitle:articleTitle,

editor:editor, badWord:matchesPlain, badDiffFragment:highlighted, diff:diffText, summary:editSummary

};

}

} else if (recent2.filter_spelling) {

var splMatch=null;

while (diffCell=diffCellRe.exec(diffText)) {

if (splMatch=spellRe.test(diffCell[1])) { break; }

}

if (splMatch) {

splMatch = diffCell[1].match(spellRe);

var misspelling = splMatch[1]; //.replace(/^\s*/, '');

var badWord = '

'","'+misspelling.split("'").join("%27")+'")\'>'+ misspelling + '';

diffText = diffText.replace(RegExp('('+misspelling+')', 'gi'),

'$1');

// linkify

diffText=recent2.doLinkify(diffText);

recent2.delayedLines[article] = {

timestamp: timestamp, article:article, count:recent2.count, articleTitle:articleTitle,

editor:editor, badWord:badWord, badDiffFragment:'', diff:diffText, summary: editSummary

};

}

} else {

var article=getFirstTagContent(items[i], 'link');

var articleTitle=getFirstTagContent(items[i], 'title');

if (recent2.CustomFilter &&

! recent2.CustomFilter({timestamp:timestamp, article:article, articleTitle:articleTitle,

editor:editor, diff:diffText, summary:editSummary})) { nextChangeSoon(); return; }

// linkify

diffText=recent2.doLinkify(diffText);

recent2.delayedLines[article]={

timestamp: timestamp, article:article, count:recent2.count, articleTitle:articleTitle,

editor:editor, diff:diffText, summary:editSummary

};

}

// schedule next iteration

nextChangeSoon();

return;

}

window.processRecentChangesDisplay=function(bundle){

var output=recent2.getDelayedLineOutput();

//console.log(output);

var outputString='';

if (recent2.outputPosition=='top') {

outputString=output.join(recent2.outputSeparator);

}

else {

for (var i=output.length-1; i>=0; --i) {

outputString+=output[i] + (i>0 ? recent2.outputSeparator : '') ;

}

}

bundle.output.innerHTML+=outputString;

if (recent2.wait_for_output) { recent2.pauseOutput(); }

setTimeout(function() {recent2.doPopups(bundle.output)}, 300);

// overlap better than missing some out, i think; FIXME do this properly

processRecentChanges.lastDate=recent2.latest; // - 1;

var statusTail=greyFont+'done up to ' + formatTime(recent2.latest) + '';

if (processRecentChanges.id > recent2.initialId) {

statusTail+=' toggle these details |';

if (recent2.autoexpand) {

setTimeout( function() {

/* document.title=initialId+' '+processRecentChanges.id; */

showHideDetailRange(recent2.initialId, processRecentChanges.id); }, 250 );

}

}

statusTail += ' remove earlier output';

if (recent2.wait_for_output) {

statusTail += ' | show new output';

}

statusTail+='
';

bundle.status.innerHTML+=statusTail;

return;

}

// linkify and popupsify wikilinks

recent2.doLinkify=function(txt) {

if (!txt || !recent2.linkify) { return txt; }

var inheritColor='color:inherit;color:expression(parentElement.currentStyle.color)';

var externalLinkStyle='text-decoration:none;'

var internalLinkStyle='text-decoration:none;'

externalLinkStyle=internalLinkStyle='text-decoration:none;border-bottom: 1px dotted;';

txt=txt.replace(/((https?|ftp):(\/\/[^\[\]\{\}\(\)<>\s&=\?#%]+|<[^>]*>)+)/g, function (p,p1) {

p1=p1.replace(/<[^>]*>/g, '');

var url=encodeURI(p1);

url=url.replace(/\"/g, '%22');

url=url.replace(/\'/g, '%27');

url=url.replace(/#/g, '%23');

var ti=p1.replace(/\"/g, '"');

return(''+p+'');

});

// BUG: doLinkify('123 badword blah blah')

// gives '123 badword blah blah'

// and the browser closes the inside the , so the badword is not red!

txt=txt.replace(/((\[\[)([^\|\[\]\{\}\n]*)([^\]\n]*)(]\]))/g, function (p,p1,p2,p3) {

p3=p3.replace(/<[^>]*>/g, '');

var url=encodeURI(p3);

url=url.replace(/\"/g, '%22');

url=url.replace(/\'/g, '%27');

url=url.replace(/#/g, '%23');

url=recent2.articlePath+url;

var ti=p3.replace(/\"/g, '"');

return(''+p+'');

});

return(txt);

}

processRecentChanges.lastDate=0;

processRecentChanges.id=0;

recent2.getDelayedLineOutput=function() {

var ret=[];

var id=processRecentChanges.id;

for (var a in recent2.delayedLines) {

if (recent2.delayedLines[a] && typeof recent2.delayedLines[a].count == typeof 1 &&

recent2.count - recent2.delayedLines[a].count >= recent2.delay) {

recent2.delayedLines[a].id=id++;

var line=(recent2.doLine(recent2.delayedLines[a]));

if (line) { ret.push(line); }

delete recent2.delayedLines[a];

}

}

processRecentChanges.id=id;

return ret;

}

window.deleteEarlierOutputDivs=function(cur) {

for(var i=0; i

if (!outputDivs[i] || !outputDivs[i].id) continue;

if (outputDivs[i].id >= 0 && outputDivs[i].id < cur) {

// FIXME BUG: if we go from the bottom up, then we'll delete one too many or too few, or something :-)

outputDivs[i].parentNode.removeChild(outputDivs[i]);

outputDivs[i]=null;

}

}

// scroll to the top if we're appending output to the bottom, to keep the div we've clicked visible after the deletions

if (recent2.outputPosition!='top') document.location='#';

}

window.showHideDetailRange=function(start,end) {

// use the first div to see if we should show or hide

var div=document.getElementById('diff_div_' + start);

if (!div) {alert('no such div: diff_div_' + start); return; }

var state=false; // hide

if (div.style.display=='none') state=true; // show

for (var i=start; i

showHideDetail(i, true, state);

}

}

window.hideSysopEdits=function(hide) {

var divs=document.getElementsByTagName('div');

for (var i=0; i

if (divs[i].className=='sysop_edit_line') divs[i].style.display= ( hide ? 'none' : 'inline' );

}

}

window.bundles={};

window.vandalColour = function(vandal) {

var num=window.vandals[vandal];

if (!num) return '';

switch (num) {

case 1: return '#DDFFDD';

case 2: return '#BBFFBB';

}

var i= 9-(num - 3) *2;

if (i < 0) i=0;

return '#' + i + i + 'FF' + i + i;

}

window.clickDetails=function(action, max) {

if(!action) action='show';

if (!max) max = document.links.length;

var count=0;

for (var i=0; i

if(document.links[i].innerHTML==action + ' details' && document.links[i].href.indexOf('javascript:') == 0) {

++count;

eval(document.links[i].href.replace('javascript:', ''));

}

}

}

recent2.pendingLines=[];

recent2.unpauseOutputOnce=function() {

//console.log('unpausing once');

if (recent2.pausedOutput) {

recent2.togglePausedOutput();

recent2.togglePausedOutput();

}

};

recent2.pauseOutput=function() {

//console.log('pausing');

if (!recent2.pausedOutput) { recent2.togglePausedOutput(); }

//console.log(recent2.pausedOutput);

}

recent2.unpauseOutput=function() {

//console.log('unpausing');

if (recent2.pausedOutput) { recent2.togglePausedOutput(); }

//console.log(recent2.pausedOutput);

}

recent2.togglePausedOutput=function() {

if (!recent2.pausedOutput) { recent2.pausedOutput = true; return true; }

else recent2.pausedOutput=false;

var outputBuffer='';

while (recent2.pendingLines.length) {

outputBuffer+=recent2.doLine(recent2.pendingLines.pop());

if (recent2.pendingLines.length) { outputBuffer+=recent2.outputSeparator; }

}

var pos=recent2.outputPosition;

var output=newOutputDiv('recent2.lines', pos);

output.innerHTML=outputBuffer;

setTimeout(function() {recent2.doPopups(output)}, 300);

return false;

}

recent2.togglePaused=function() {

if(!recent2.paused) { recent2.paused=true; return true; }

recent2.paused=false;

loopRecentChanges(loopRecentChanges.url, loopRecentChanges.iterations);

return false;

}

recent2.doLine=function(bundle) {

if (recent2.pausedOutput) {

recent2.pendingLines.push(bundle);

return '';

}

//if (recent2.filter_spelling) { return recent2.doSpellLine(bundle); }

var sysop = null;

if (typeof(recent2.sysopRegExp != 'undefined')) {

sysop=recent2.sysopRegExp.test(bundle.editor);

}

var lastDiffPage=bundle.article + '&diff=cur&oldid=prev';

bundle.url=lastDiffPage;

saveBundle(bundle);

var div='';

var group='';

if (window.vandals[bundle.editor]) {

if (window.vandals[bundle.editor] > 0) {

div='

';

}

}

else if (sysop) {

group = ' (Admin)';

if (recent2.hide_sysop_edits) {

div='