User:Novem Linguae/Scripts/UserTalkErasedSectionsDetector.js
Appearance
Code that you insert on this page could contain malicious content capable of compromising your account. If you import a script from another page with "importScript", "mw.loader.load", "iusc", or "lusc", take note that this causes you to dynamically load a remote script, which could be changed by others. Editors are responsible for all edits and actions they perform, including by scripts. User scripts are not centrally supported and may malfunction or become inoperable due to software changes. A guide to help you find broken scripts is available. If you are unsure whether code you are adding to this page is safe, you can ask at the appropriate village pump. This code will be executed when previewing this page. |
Documentation for this user script can be added at User:Novem Linguae/Scripts/UserTalkErasedSectionsDetector. |
// <nowiki>
/*
A user script that alerts you with a yellow banner at the top of a User Talk page if more than 3% of recent user talk diffs are self-deletions, with exceptions for some edit summary keywords such as "archiving".
Useful for detecting if a WP:PERM applicant is whitewashing their User Talk page by removing warnings without archiving them.
*/
class ErasedSectionsDetector {
constructor( mw, $ ) {
this.mw = mw;
// eslint-disable-next-line no-jquery/variable-pattern
this.$ = $;
}
async execute() {
if ( !this.shouldRunOnThisPage() ) {
return;
}
const title = this.mw.config.get( 'wgPageName' ).replace( /_/g, ' ' );
this.revisions = await this.getRevisions( title );
const totalRevisionCount = this.revisions.length;
this.addDiffsToRevisions();
this.filterForRevisionsByThisEditorOnly();
this.filterForContentRemoval();
this.filterOutReasonableEditSummaries();
this.expandBlankEditSummaries();
const negativeDiffCount = this.revisions.length;
const deletionPercent = negativeDiffCount / totalRevisionCount;
const MINIMUM_DELETION_PERCENT = 0.03;
if ( deletionPercent > MINIMUM_DELETION_PERCENT ) {
this.addHtml( negativeDiffCount, totalRevisionCount );
this.listenForShowDiffsClick();
}
}
/**
* Add a message to blank edit summaries. This is so the hyperlink can be clicked.
*/
expandBlankEditSummaries() {
this.revisions = this.revisions.map( ( revision ) => {
if ( revision.comment === '' ) {
revision.comment = '[no edit summary]';
}
return revision;
} );
}
listenForShowDiffsClick() {
this.$( '#ErasedSectionsDetector-SeeDiffs' ).on( 'click', () => {
this.$( '#ErasedSectionsDetector-Diffs' ).toggle();
} );
}
addHtml( negativeDiffCount, totalRevisionCount ) {
let html = `
<div class="ErasedSectionsDetector">
<div style="background-color: yellow">
<span style="font-weight:bold">Warning:</span> This user has removed content from this page (probably without archiving it) in ${ negativeDiffCount } of the last ${ totalRevisionCount } revisions. <a id="ErasedSectionsDetector-SeeDiffs">Click here</a> to see diffs.
</div>
<div id="ErasedSectionsDetector-Diffs" style="border: 1px solid black; font-size: 80%; display: none;">
<ul>
`;
for ( const revision of this.revisions ) {
html += `
<li>
<a href="w/index.php?title=${ encodeURIComponent( this.mw.config.get( 'wgPageName' ) ) }&diff=prev&oldid=${ revision.revid }">
${ revision.comment }
</a>
</li>
`;
}
html += `
</ul>
</div>
</div>
`;
this.$( '#contentSub2' ).after( html );
}
filterForContentRemoval() {
const MINIMUM_DIFF_SIZE = -10;
this.revisions = this.revisions.filter( ( revision ) => revision.diff < MINIMUM_DIFF_SIZE );
}
filterForRevisionsByThisEditorOnly() {
const thisEditor = this.mw.config.get( 'wgTitle' );
this.revisions = this.revisions.filter( ( revision ) => revision.user === thisEditor );
}
filterOutReasonableEditSummaries() {
const keywordsToIgnore = [
'arc', // arc, arch, archive, archiving, OneClickArchiver
'bot mes', // mesg, message
'mass mes',
'newsletter',
'wikibreak',
'out of town'
];
for ( let keyword of keywordsToIgnore ) {
this.revisions = this.revisions.filter( ( revision ) => {
keyword = keyword.toLowerCase();
const editSummary = revision.comment.toLowerCase();
return !editSummary.includes( keyword );
} );
}
}
/**
* Given the Action API output of query revisions as a JavaScript object, add to this object a field called "diff" that is the difference +/- in size of that diff compared to the next oldest diff.
*/
addDiffsToRevisions() {
const len = this.revisions.length;
let lastRevisionSize = this.revisions[ len - 1 ].size;
// need to store the OLDER revision's size in a buffer to compute a diff, so iterate BACKWARDS
for ( let i = ( len - 2 ); i >= 0; i-- ) {
const thisRevisionSize = this.revisions[ i ].size;
this.revisions[ i ].diff = thisRevisionSize - lastRevisionSize;
lastRevisionSize = thisRevisionSize;
}
}
async getRevisions( title ) {
const api = new this.mw.Api();
const response = await api.get( {
action: 'query',
format: 'json',
prop: 'revisions',
titles: title,
formatversion: '2',
rvprop: 'comment|size|user|ids',
rvslots: '',
rvlimit: '500', // get 500 revisions
rvdir: 'older' // get newest revisions (enumerate towards older entries)
} );
return response.query.pages[ 0 ].revisions;
}
shouldRunOnThisPage() {
const isViewing = this.mw.config.get( 'wgAction' ) === 'view';
if ( !isViewing ) {
return false;
}
const isDiff = this.mw.config.get( 'wgDiffNewId' );
if ( isDiff ) {
return false;
}
const isDeletedPage = !this.mw.config.get( 'wgCurRevisionId' );
if ( isDeletedPage ) {
return false;
}
const namespace = this.mw.config.get( 'wgNamespaceNumber' );
const isUserTalkNamespace = [ 3 ].includes( namespace );
if ( !isUserTalkNamespace ) {
return false;
}
const isSubPage = this.mw.config.get( 'wgPageName' ).includes( '/' );
if ( isSubPage ) {
return;
}
return true;
}
}
$( async () => {
await mw.loader.using( [ 'mediawiki.api' ], async () => {
await ( new ErasedSectionsDetector( mw, $ ) ).execute();
} );
} );
// </nowiki>