User:SD0001/libApi.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:SD0001/libApi. |
/**
* libApi for browser JS
* Library of functions to work with mw.Api easily.
*
* For usage of equivalent functions in Node.js, use the mwn bot
* framework, <https://github.com/siddharthvp/mwn>
*
* @author SD0001
*
*/
/**
* Send an API query that automatically continues till the limit is reached.
*
* @param {mw.Api} mwApi - the mw.Api object used for API calls
* @param {Object} query - The API query
* @param {number} [limit=10] - limit on the maximum number of API calls to go through
* @returns {jQuery.Promise<Object[]>} - resolved with an array of responses of individual calls.
*/
var ApiQueryContinuous = function(mwApi, query, limit) {
limit = limit || 10;
var responses = [];
var callApi = function(query, count) {
return mwApi.get(query).then(function(response) {
responses.push(response);
if (response.continue && count < limit) {
return callApi($.extend({}, query, response.continue), count + 1);
} else {
return responses;
}
});
};
return callApi(query, 1);
};
/**
* Function for using API action=query with more than 50/500 items in multi-input fields.
*
* Several fields in the query API take multiple inputs but with a limit of 50 (or 500 for bots).
* Example: the fields titles, pageids and revids in any query, ususers in list=users,
* clcategories in prop=categories, etc.
*
* This function allows you to send a query as if this limit didn't exist. The array given to
* the multi-input field is split into batches of 50 (500 for bots) and individual queries are
* sent sequentially for each batch. A promise is returned finally resolved with the array of
* responses of each API call.
*
* @param {mw.Api} mwApi - mw.Api object to use for the API calls
* @param {Object} query - the query object, the multi-input field should be an array
* @param {string} [batchFieldName=titles] - the name of the multi-input field
* @returns {jQuery.Promise<Object[]>} - promise resolved when all the API queries have settled,
* with the array of responses.
* @requires mediawiki.api
*/
var ApiMassQuery = function(mwApi, query, batchFieldName) {
batchFieldName = batchFieldName || 'titles';
var batchValues = query[batchFieldName];
var hasApiHighLimit = (mw.config.get('wgUserGroups').indexOf('sysop') !== -1 ||
mw.config.get('wgUserGroups').indexOf('bot') !== -1);
var limit = hasApiHighLimit ? 500 : 50;
var numBatches = Math.ceil(batchValues.length / limit);
var batches = new Array(numBatches);
for (var i = 0; i < numBatches; i++) {
batches[i] = new Array(limit);
}
for (var i = 0; i < batchValues.length; i++) {
batches[Math.floor(i/limit)][i % limit] = batchValues[i];
}
var responses = new Array(numBatches);
var deferred = $.Deferred();
var sendQuery = function(idx) {
if (idx === numBatches) {
deferred.resolve(responses);
return;
}
query[batchFieldName] = batches[idx];
mwApi.get(query).done(function(response) {
responses[idx] = response;
}).always(function() {
sendQuery(idx + 1);
});
};
sendQuery(0);
return deferred;
};
/**
* Execute an asynchronous function on a large number of pages (or other arbitrary items).
* Similar to Morebits.batchOperation in [[MediaWiki:Gadget-morebits.js]], but designed for
* working with promises.
*
* @param {Array} list - list of items to execute actions upon. The array would
* usually be of page names (strings).
* @param {Function} worker - function to execute upon each item in the list. Must
* return a promise.
* @param {number} [batchSize=50] - number of concurrent operations to take place.
* Set this to 1 for sequential operations. Default 50. Set this according to how
* expensive the API calls made by worker are.
* @param {HTMLElement} [statusElement] - HTML element in which to show the status
* message
* @returns {jQuery.Promise} - resolved when all API calls have finished.
*/
var ApiBatchOperation = function(list, worker, batchSize, statusElement) {
batchSize = batchSize || 50;
if (statusElement) {
statusElement.textContent = `Finished 0/${list.length} (0%) tasks, of which 0 (0%) were successful, and 0 failed.`;
}
var successes = 0, failures = 0;
var incrementSuccesses = function() { successes++; };
var incrementFailures = function() { failures++; };
var updateStatusText = function() {
var percentageFinished = Math.round((successes + failures) / list.length * 100);
var percentageSuccesses = Math.round(successes / (successes + failures) * 100);
var statusText = `Finished ${successes + failures}/${list.length} (${percentageFinished}%) tasks, of which ${successes} (${percentageSuccesses}%) were successful, and ${failures} failed.`;
if (statusElement) {
statusElement.textContent = statusText;
} else {
console.log(statusText);
}
}
var returnPromise = $.Deferred();
var numBatches = Math.ceil(list.length / batchSize);
var sendBatch = function(batchIdx) {
if (batchIdx === numBatches - 1) { // last batch
var cnt = 0;
var numItemsInLastBatch = list.length - batchIdx * batchSize;
var finalBatchPromises = new Array(numItemsInLastBatch);
for (var i = 0; i < numItemsInLastBatch; i++) {
finalBatchPromises[i] = $.Deferred();
var idx = batchIdx * batchSize + i;
var promise = worker(list[idx]);
promise.then(incrementSuccesses, incrementFailures).always(function() {
finalBatchPromises[cnt++].resolve();
updateStatusText();
});
}
$.when.apply($, finalBatchPromises).then(returnPromise.resolve);
return;
}
for (var i = 0; i < batchSize; i++) {
var idx = batchIdx * batchSize + i;
var promise = worker(list[idx]);
promise.then(incrementSuccesses, incrementFailures).always(updateStatusText);
if (i === batchSize - 1) { // last item in batch: trigger the next batch's API calls
promise.always(function() {
sendBatch(batchIdx + 1);
});
}
}
};
sendBatch(0);
return returnPromise;
};
/**
* @class
* ** UNTESTED **
* Re-try an API request one more time if it fails.
* Or neither to unconditionally attempt a retry on failure.
* @param {mw.Api} mwApi - the mw.Api object to send API calls with
*/
var ApiWithRetry = function(mwApi) {
// Set either on of these:
this.retryOnErrors = null;
this.dontRetryOnErrors = null;
/**
* @param {string} method - mw.Api method to use
* @param {...*} method arguments
*/
this.send = function(method) {
var args = Array.prototype.slice.call(arguments, 1);
return mwApi[method].apply(null, args).catch(function(errorCode) {
var otherArgs = Array.prototype.slice.call(arguments, 1);
if ((this.dontRetryOnErrors && !this.dontRetryOnErrors.includes(errorCode)) ||
(this.retryOnErrors && this.retryOnErrors.includes(errorCode)) ||
(!this.retryOnErrors && !this.dontRetryOnErrors)) {
return mwApi[method].apply(null, args);
} else {
return $.Deferred().reject.apply(null, [errorCode].concat(otherArgs));
}
});
};
};
window.ApiQueryContinuous = ApiQueryContinuous;
window.ApiMassQuery = ApiMassQuery;
window.ApiBatchOperation = ApiBatchOperation;
window.ApiWithRetry = ApiWithRetry;