MediaWiki:Gadget-MarkAdmins-updater.js
/**
* Updates the JSON data for .
* @module
* @link https://ja.wikipedia.org/wiki/MediaWiki:Gadget-MarkAdmins.js
* @link https://ja.wikipedia.org/wiki/MediaWiki:Gadget-MarkAdmins-data.json
*
* Note that data updating should be done by sysops because of the current limitation of mediawiki action API.
* list=allusers and list=globalallusers limit their results to 5000 entries for users with apihighlimits, and
* 500 entries for those without it. The current version of this script doesn't handle API request continuation
* because there is an issue with list=globalallusers that the response JSON doesn't have a "continue" property
* (see T241940 on phabricator).
*/
//
(function(mw, $) {
// *************************************************************************************************************
// Is the current user a sysop?
if (mw.config.get('wgUserGroups').indexOf('sysop') === -1) return;
// Define user groups for which we collect usernames.
// Note that the order of items affects the order of markers.
var localGroups = [
'sysop',
'suppress',
'checkuser',
'bureaucrat',
'interface-admin',
'abusefilter',
'accountcreator',
'bot',
'eliminator',
'rollbacker'
];
var globalGroups = [
'founder',
'steward',
'ombuds',
'staff',
'sysadmin',
'global-sysop',
'abusefilter-maintainer',
'abusefilter-helper',
'global-interface-editor',
'global-bot',
'global-deleter',
'global-rollbacker',
'vrt-permissions'
];
var metaGroups = [
'global-renamer',
'wmf-officeit',
'wmf-supportsafety'
];
var allGroups = localGroups.concat(globalGroups, metaGroups);
// Load dependent modules
var api, fApi;
$.when(
mw.loader.using(['mediawiki.api', 'mediawiki.ForeignApi', 'mediawiki.util', 'mediawiki.notification']),
$.ready
).then(function() {
api = new mw.Api();
fApi = new mw.ForeignApi('https://meta.wikimedia.org/w/api.php');
var portlet = mw.util.addPortletLink('p-cactions', '#', 'MarkAdmins-updater', 'ca-mau', null, null, '#ca-move');
portlet.addEventListener('click', updateJson);
});
// *********************************************************************************************************
/**
* Main function to update the JSON. Trigerred when the portletlink is hit.
* @param {Event} e
*/
function updateJson(e) {
e.preventDefault();
var autoUpdate = confirm('MarkAdminsのJSONデータを取得します。更新が必要な際、自動編集を行う場合は "OK" を、行わない場合は "Cancel" を押してください。');
mw.notification.notify('JSONデータを取得しています...');
getUsersInGroups().then(function(groups) {
if (groups) {
console.log(groups);
mw.notification.notify('JSONデータをコンソールに出力しました');
} else {
mw.notification.notify('JSONデータの取得に失敗しました');
return;
}
var pagetitle = 'MediaWiki:Gadget-MarkAdmins-data.json';
getLatestRevision(pagetitle).then(function(resObj) {
if (!resObj) return;
var groupsOld = JSON.parse(resObj.content);
if (isSameDataObject(groups, groupsOld)) {
mw.notification.notify('データは最新のため更新不要です');
return;
} else if (!autoUpdate) {
mw.notification.notify('データの更新が必要です');
return;
} else {
mw.notification.notify('データを更新しています...');
}
api.postWithEditToken({
action: 'edit',
title: pagetitle,
text: JSON.stringify(groups, null, 4),
summary: 'データの更新 (MarkAdmins-updater)',
basetimestamp: resObj.basetimestamp,
starttimestamp: resObj.curtimestamp,
formatversion: '2'
}).then(function(res) {
if (res && res.edit && res.edit.result === 'Success') {
mw.notification.notify('データを更新しました');
} else {
mw.notification.notify('データの更新に失敗しました (不明なエラー)');
}
}).catch(function(code, err) {
console.error(err);
var msg = err && err.error && err.error.info ? ' (' + err.error.info + ')' : ;
mw.notification.notify('データの更新に失敗しました' + msg);
});
});
});
}
/**
* Reduce an array of objects fetched from the API to an array of objects with specific properties
* @param {Array<{id: number, name: string, groups: Array}>} responseArray API response array in res.query[keyname]
* @param {Array} groupsArray
* @returns {Array}
* @typedef UserGroup
* @type {object}
* @property {string} name
* @property {Array} groups The array is filtered
*/
function reduceResponse(responseArray, groupsArray) {
/** @type {Array} */
var arr = [];
return responseArray.reduce(function(acc, obj) {
acc.push({
name: obj.name,
groups: obj.groups.filter(function(group) {
return groupsArray.indexOf(group) !== -1; // Only leave relevant group names
})
});
return acc;
}, arr);
}
/**
* @param {"local"|"global"|"meta"} groupType
* @returns {JQueryPromise>}
*/
function queryUserGroups(groupType) {
var def = $.Deferred();
var listType, groupsArray, API;
var params = {
action: 'query',
list: (listType = (groupType === 'global' ? 'globalallusers' : 'allusers')),
formatversion: '2'
};
switch(groupType) {
case 'local':
$.extend(params, {
aulimit: 'max',
augroup: (groupsArray = localGroups).join('|'),
auprop: 'groups'
});
API = api;
break;
case 'global':
$.extend(params, {
agulimit: 'max',
agugroup: (groupsArray = globalGroups).join('|'),
aguprop: 'groups',
});
API = api;
break;
case 'meta':
$.extend(params, {
aulimit: 'max',
augroup: (groupsArray = metaGroups).join('|'),
auprop: 'groups'
});
API = fApi;
break;
default:
return def.resolve();
}
API.get(params)
.then(function(res) {
var resArray;
if (!res || !res.query || !(resArray = res.query[listType]) || resArray.length === 0) {
def.resolve();
} else {
def.resolve(reduceResponse(resArray, groupsArray));
}
}).catch(function(code, err) {
console.error(err);
def.resolve();
});
return def.promise();
}
/**
* @returns {JQueryPromise>>}
*/
function getUsersInGroups() {
return $.when.apply($, [
queryUserGroups('local'),
queryUserGroups('global'),
queryUserGroups('meta')
]).then(function() {
var args = arguments,
resLocal = args[0],
resGlobal = args[1],
resMeta = args[2];
if (!resLocal || !resGlobal || !resMeta) return;
var resAll = resLocal.concat(resGlobal, resMeta);
return resAll.reduce(function(acc, obj) {
var username, groupsArr;
if ((username = obj.name) && (groupsArr = obj.groups) && Array.isArray(obj.groups)) {
groupsArr = groupsArr.sort(function(a, b) {
return allGroups.indexOf(a) - allGroups.indexOf(b);
});
if (!acc[username]) {
acc[username] = groupsArr;
} else {
acc[username] = acc[username].concat(groupsArr);
}
}
return acc;
}, Object.create(null));
});
}
/**
* @param {string} pagetitle
* @returns {JQueryPromise}
*/
function getLatestRevision(pagetitle) {
var def = $.Deferred();
api.get({
action: 'query',
titles: pagetitle,
prop: 'revisions',
rvprop: 'timestamp|content|ids',
rvslots: 'main',
curtimestamp: 1,
formatversion: '2'
}).then(function(res) {
var resPages;
if (!res || !res.query || !(resPages = res.query.pages) || resPages.length === 0) {
return def.resolve();
}
resPages = resPages[0];
def.resolve({
title: resPages.title,
missing: resPages.missing,
revid: resPages.missing ? undefined : resPages.revisions[0].revid.toString(),
basetimestamp: resPages.missing ? undefined : resPages.revisions[0].timestamp,
curtimestamp: res.curtimestamp,
content: resPages.missing ? undefined : resPages.revisions[0].slots.main.content
});
}).catch(function(code, err) {
console.error(err);
def.resolve();
});
return def.promise();
}
/**
* @param {Array<(boolean|string|number|undefined|null)>} array1
* @param {Array<(boolean|string|number|undefined|null)>} array2
* @param {boolean} [orderInsensitive] If true, ignore the order of elements
* @returns {boolean|null} Null if non-arrays are passed as arguments
*/
function arraysEqual(array1, array2, orderInsensitive) {
if (!Array.isArray(array1) || !Array.isArray(array2)) {
return null;
} else if (orderInsensitive) {
return array1.length === array2.length && array1.every(function(el) {
return array2.indexOf(el) !== -1;
});
} else {
return array1.length === array2.length && array1.every(function(el, i) {
return array2[i] === el;
});
}
}
/**
* @param {object} dataObj1
* @param {object} dataObj2
* @returns {boolean}
*/
function isSameDataObject(dataObj1, dataObj2) {
if (!arraysEqual(Object.keys(dataObj1), Object.keys(dataObj2), true)) {
return false;
} else {
return Object.keys(dataObj1).every(function(key) {
var propArr1 = dataObj1[key];
var propArr2 = dataObj2[key];
return arraysEqual(propArr1, propArr2);
});
}
}
// *************************************************************************************************************
// @ts-ignore "Cannot find name 'mediaWiki'."
})(mediaWiki, jQuery);
//