MediaWiki:Gadget-MarkAdmins-updater.js

お知らせ: 保存した後、ブラウザのキャッシュをクリアしてページを再読み込みする必要があります。

多くの WindowsLinux のブラウザ

  • Ctrl を押しながら F5 を押す。

Mac における Safari

  • Shift を押しながら、更新ボタン をクリックする。

Mac における ChromeFirefox

  • Cmd Shift を押しながら R を押す。

詳細についてはWikipedia:キャッシュを消すをご覧ください。

/**
 * Updates the JSON data for [[MediaWiki:Gadget-MarkAdmins.js]].
 * @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).
 */
//<nowiki>

(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(
        document.getElementById('p-cactions') ? 'p-cactions' : 'p-personal',
        '#',
        'MarkAdmins-updater',
        'ca-mau'
    );
    if (portlet) {
        portlet.addEventListener('click', updateJson);
    } else {
        console.error('Failed to create a portlet link for MarkAdmins-updater.');
    }
});

// *********************************************************************************************************

/**
 * 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: 'データの更新 ([[H:MA#UPDATER|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<string>}>} responseArray API response array in res.query[keyname]
 * @param {Array<string>} groupsArray
 * @returns {Array<UserGroup>}
 * @typedef UserGroup
 * @type {object}
 * @property {string} name
 * @property {Array<string>} groups The array is filtered
 */
function reduceResponse(responseArray, groupsArray) {
    /** @type {Array<UserGroup>} */
    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<undefined|Array<UserGroup>>}
 */
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<undefined|Object.<string, Array<string>>>}
 */
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<undefined|{title: string, missing: boolean, revid: string, basetimestamp: string, curtimestamp: string, content: string}>}
 */
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);
//</nowiki>