User:JPxG/SPS.js

/*

Signpost Publishing Script (SPS)

by Evad37

Forked by JPxG, 2022

------------

Note 1: This script will only run for users specified in the publishers array.

------------

Note 2: This script assumes users have the following permissions - you must request them if you do

not already have them. THIS IS IMPORTANT! THE SCRIPT WILL EAT SHIT AND MESS UP THE ISSUE IF YOU RUN IT WITHOUT THEM!

* Page mover (or administrator) on English Wikipedia

- This ensures redirects are not left behind when moving pages during publication.

* Mass message sender (or administrator) on English Wikipedia

- This allows posting the Signpost on the talkpages of English Wikipedia subscribers.

* Mass message sender (or administrator) on Meta

- This allows posting the Signpost on the talkpages of subscribers on other projects.

  • /

/* jshint esversion: 6, esnext:false, laxbreak: true, undef: true, maxerr: 999 */

/* globals console, window, document, $, mw, OO, extraJs */

//

/* ========== Dependencies and initial checks =================================================== */

$.when(

// Resource loader modules

mw.loader.using([

'mediawiki.util', 'mediawiki.api', 'ext.gadget.libExtraUtil',

'oojs-ui-core', 'oojs-ui-widgets', 'oojs-ui-windows'

]),

// Page ready

$.ready

).then(function() {

var atNewsroom = mw.config.get('wgPageName').includes('Wikipedia:Wikipedia_Signpost/Newsroom');

if ( !atNewsroom ) {

return;

}

var publishers = ['Evad37', 'Chris troutman', 'Smallbones', 'Bri', 'Eddie891', 'DannyS712', 'JPxG', 'EpicPupper'];

var isApprovedUser = ( publishers.indexOf(mw.config.get('wgUserName')) !== -1 );

if ( !isApprovedUser ) {

return;

}

// Script version and API config options

var scriptVersion = '2.4.3';

var apiConfig = {ajax: {headers: {'Api-User-Agent': 'SignpostPublishingScript/' + scriptVersion + ' ( https://en.wikipedia.org/wiki/User:Evad37/SPS )'} } };

window.SPSdebug = true;

//On first run after page load, clear the cache in window.localStorage

try {

window.localStorage.setItem('SignpostPubScript-titles', '');

window.localStorage.setItem('SignpostPubScript-selected-titles', '');

window.localStorage.setItem('SignpostPubScript-previousIssueDates', '');

window.localStorage.setItem('SignpostPubScript-info', '');

window.localStorage.setItem('SignpostPubScript-startAtZero', '');

} catch(e) {}

/* ========== Styles ============================================================================ */

mw.util.addCSS(

'.SPS-dialog-heading { font-size: 115%; font-weight: bold; text-align: center; margin: -0.2em 0 0.2em; }'+

'.SPS-dryRun { display: none; font-size: 88%; margin-left: 0.2em; }'+

'.SPS-dialog-DraggablePanel { margin: 0.5em 0; }'+

'.SPS-dialog-DraggablePanel .oo-ui-fieldLayout.oo-ui-fieldLayout-align-left > .oo-ui-fieldLayout-body > .oo-ui-fieldLayout-header { display: none; }'+

'.SPS-dialog-item-section { font-weight: bold; }'+

'.SPS-dialog-item-title { font-size: 92%; color: #333; margin-left: 0.1em; }'+

'.SPS-dialog-item-blurb { font-size: 85%; color: #333; margin-left: 0.1em; }'+

'.four ul li.SPS-task-waiting { color: #777; }'+

'.four ul li.SPS-task-doing { color: #00F; }'+

'.four ul li.SPS-task-done { color: #0A0; }'+

'.four ul li.SPS-task-failed { color: #A00; }'+

'.four ul li.SPS-task-skipped { color: #B0A; }'+

'.four ul li .SPS-task-status { font-weight: bold; }'+

'.four ul li .SPS-task-failed .SPS-task-errorMsg { font-weight: bold; }'+

'.SPS-inlineButton { margin: 0.2em; padding: 0.3em 0.6em; font-size: 0.9em; }'+

'.no-bold { font-weight: normal; }'

);

/* ========== Utility functions ================================================================= */

/** writeToCache

* @param {String} key

* @param {Array|Object} val

*/

var writeToCache = function(key, val) {

try {

var stringVal = JSON.stringify(val);

window.localStorage.setItem('SignpostPubScript-'+key, stringVal);

} catch(e) {}

};

/** readFromCache

* @param {String} key

* @returns {Array|Object|String|null} Cached array or object, or empty string if not yet cached,

* or null if there was error.

*/

var readFromCache = function(key) {

var val;

try {

var stringVal = window.localStorage.getItem('SignpostPubScript-'+key);

if ( stringVal !== '' ) {

val = JSON.parse(stringVal);

}

} catch(e) {

console.log('[SPS] error reading ' + key + ' from window.localStorage cache:');

console.log(

'\t' + e.name + ' message: ' + e.message +

( e.at ? ' at: ' + e.at : '') +

( e.text ? ' text: ' + e.text : '')

);

}

return val || null;

};

/** promiseTimeout

* @param {Number} time - duration of the timeout in miliseconds

* @returns {Promise} that will be resolved after the specified duration

*/

var promiseTimeout = function(time) {

var timeout = $.Deferred();

window.setTimeout(function() { return timeout.resolve(); }, time);

return timeout;

};

/** reflect

* @param {Promise|Any} Promise, or a value to treated as the result of a resolved promise

* @returns {Promise} that always resolves to an object which wraps the values or errors from the

* resolved or rejected promise in a 'value' or 'error' array, along with a 'status' of

* "resolved" or "rejected"

*/

var reflect = function(promise) {

var argsArray = function(args) {

return (args.length === 1 ? [args[0]] : Array.apply(null, args));

};

return $.when(promise).then(

function() { return {'value': argsArray(arguments), status: "resolved" }; },

function() { return {'error': argsArray(arguments), status: "rejected" }; }

);

};

/** whenAll

* Turns an array of promises into a single promise of an array, using $.when.apply

* @param {Promise[]} promises

* @returns {Promise} resolved promises

*/

var whenAll = function(promises) {

return $.when.apply(null, promises).then(function() {

return Array.from(arguments);

});

};

/** getFullUrl

* @param {String|null} page Page name. Defaults to the value of `mw.config.get('wgPageName')`

* @param {Object|null} params A mapping of query parameter names to values, e.g. `{action: 'edit'}`

* @retuns {String} Full url of the page

*/

var getFullUrl = function(page, params) {

return 'https:' + mw.config.get('wgServer') + mw.util.getUrl( page, params );

};

/** approxPageSize

* Calculates the approximate size of a page by adding up the size of images,

* and rounding up to the nearest MB.

*

* @param {String} page Name of the page

* @return {Promise} Size

*/

var approxPageSize = function(page) {

return $.get( getFullUrl(page, {useskin: 'vector'}) ).then(function(pageHtml) {

var doc = document.implementation.createHTMLDocument("New Document");

$(doc.body).append( pageHtml.replace(/<(link|meta|script.*?\>).*?\>/g, '') );

var imagesSizesPromises = Array.from(doc.images).map(function(image) {

return $.get(image.src).then(res => {

// Most image responses will have a .length prop (which slightly underestimates file size)

if (res.length) {

var approxSize = res.length*1.09; // .length slightly underestimates file size

if (!isNaN(approxSize)) {

return approxSize;

}

}

// Otherwise, we can try approximating the length of the root element (adjusted to account for compresison)

var rootHtmlLength = res && res.rootElement && res.rootElement.outerHTML && res.rootElement.outerHTML.length;

if (rootHtmlLength) {

var approxHTMLSize = rootHtmlLength * 4.2/13.37; // Based on dev tools, 4.20 KB is transferred for a 13.37 KB svg file.

if (!isNaN(approxHTMLSize)) {

return approxHTMLSize;

}

}

// If all else fails, just ignore the file

return 0;

});

});

return $.when.apply(null, imagesSizesPromises).then(function() {

var imagesSizes = Array.from(arguments);

var total_bytes = (200*1024) + // Non-image resources (scripts, css, etc)

imagesSizes.reduce((a, b) => a+b);

var total_Mb = total_bytes / 1024 / 1024;

var total_Mb_rounded = Math.round(total_Mb*10)/10;

var total_approx = (total_Mb < 0.1) ?

"<0.1 MB" :

total_Mb_rounded + " MB";

return total_approx;

});

});

};

var removeHtmlComments = function(wikitext, trim) {

var newWikitext = wikitext.replace(//g, '');

if (window.SPSdebug) {console.log("removed HTML comments");}

return trim ? newWikitext.trim() : newWikitext;

};

/* ========== Overlay Dialog ==================================================================== */

/* ---------- OverlayDialog class --------------------------------------------------------------- */

// Create OverlayDialog class that inherits from OO.ui.MessageDialog

var OverlayDialog = function( config ) {

OverlayDialog.super.call( this, config );

};

OO.inheritClass( OverlayDialog, OO.ui.MessageDialog );

// Give it a static name property

OverlayDialog.static.name = 'overlayDialog';

// Override clearMessageAndSetContent method

OverlayDialog.prototype.clearMessageAndSetContent = function(contentHtml) {

// Find the message label in the dialog

this.$element.find('label.oo-ui-messageDialog-message')

// Insert the new content after it

.after(

$('

').addClass('oo-ui-overlayDialog-content').append(contentHtml)

)

// And empty the original message

.empty();

};

// Override getTeardownProcess method

OverlayDialog.prototype.getTeardownProcess = function( data ) {

// When closing, remove the content div

this.$element.find('div.oo-ui-overlayDialog-content').remove();

// Call superclass method

return OverlayDialog.super.prototype.getTeardownProcess.call( this, data );

};

/* ---------- Window manager -------------------------------------------------------------------- */

/* Factory.

Makes it easer to use the window manager: Open a window by specifiying the symbolic name of the

class to use, and configuration options (if any). The factory will automatically crete and add

windows to the window manager as needed. If there is an old window of the same symbolic name,

the new version will automatically replace old one.

  • /

var ovarlayWindowFactory = new OO.Factory();

ovarlayWindowFactory.register( OverlayDialog );

var ovarlayWindowManager = new OO.ui.WindowManager({ factory: ovarlayWindowFactory });

ovarlayWindowManager.$element.attr('id','SPS-ovarlayWindowManager').addClass('sps-oouiWindowManager').appendTo('body');

/** showOverlayDialog

* @param {Promise} contentPromise - resolves to {String} of HTML to be displayed

* @param {String} title - title of overlay dialog

*/

var showOverlayDialog = function(contentWikitext, parsedContentPromise, title, mode) {

var isWikitextMode = mode && mode.wikitext;

var contentPromise = ( isWikitextMode ) ?

$.Deferred().resolve($('

').text(contentWikitext)) : parsedContentPromise;

var instance = ovarlayWindowManager.openWindow( 'overlayDialog', {

title: title + ( isWikitextMode ? ' wikitext' : '' ),

message: 'Loading...',

size: 'larger',

actions: [

{

action: 'close',

label: 'Close',

flags: 'safe'

},

{

action: 'toggle',

label: 'Show ' + ( isWikitextMode ? 'preview' : 'wikitext' )

}

]

});

instance.opened.then( function() {

contentPromise.done(function(contentHtml) {

ovarlayWindowManager.getCurrentWindow().clearMessageAndSetContent(contentHtml);

ovarlayWindowManager.getCurrentWindow().updateSize();

})

.fail(function(code, jqxhr) {

ovarlayWindowManager.getCurrentWindow().clearMessageAndSetContent(

'Preview failed.',

( code == null ) ? '' : extraJs.makeErrorMsg(code, jqxhr)

);

});

});

instance.closed.then(function(data) {

if ( !data || !data.action || data.action !== 'toggle' ) {

return;

}

showOverlayDialog(contentWikitext, parsedContentPromise, title, {'wikitext': !isWikitextMode});

});

};

/* ========== Fake API class ==================================================================== */

// For dry-run mode. Makes real read request to retrieve content, but logs write request to console.

// Also handles previews of content.

var FakeApi = function(apiConfig){

this.realApi = new mw.Api(apiConfig);

this.isFake = true;

};

FakeApi.prototype.abort = function() {

this.realApi.abort();

console.log('FakeApi was aborted');

};

FakeApi.prototype.get = function(request) {

return this.realApi.get(request);

};

FakeApi.prototype.preview = function(label, content, title) {

var self = this;

$('#SPS-previewButton-container').append(

$('