User:Novem Linguae/Scripts/UserTalkErasedSectionsDetector.js

//

/*

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 = `

Warning: This user has removed content from this page (probably without archiving it) in ${ negativeDiffCount } of the last ${ totalRevisionCount } revisions. Click here to see diffs.

`;

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();

} );

} );

//