利用者:MawaruNeko/StoredMarkAdmins.js

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

多くの WindowsLinux のブラウザ

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

Mac における Safari

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

Mac における ChromeFirefox

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

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

/*
 * ローカルストレージを使用したマークアドミンのカスタムJS
 * Custom JS of MarkAdmins using local storage
 * 
 * 説明:
 *   一定期間ごとにAPIで管理者一覧を自動更新し、
 *   ローカルストレージを使用することで、
 *   手作業による更新が不要になった[[Help:マークアドミン]]です。
 *   カスタムJSとして導入して下さい。
 * 
 * Description:
 *   MarkAdmins without manual updating by using local storage.
 *   Use this file as custom JS.
 * 
 * Global variables:
 *   以下のグローバル変数を、このスクリプトを読み込むより前に設定することで、
 *   このスクリプトの動作を制御できます。
 *   (default)
 *   mw.libs.storedMarkAdmins = {
 *     validTermInMs: 30 * 24 * 60 * 60 * 1000, // 30 days
 *     apiCallInterval: 1000, // 1000ms
 *     apiMaxTry: 10, // up to 5000 users for each role
 *     apiNotifyMessage: '$role一覧を取得中…', // substitute '$role' to role name, null to not to show notify
 *   };
 *   mw.libs.storedMarkAdmins.validTermInMs:
 *     管理者一覧の自動更新の期間をミリ秒で指定します。
 *     この期間が過ぎると、APIで自動的に管理者一覧を更新します。
 *   mw.libs.storedMarkAdmins.apiCallInterval:
 *     API呼び出しの間隔をミリ秒で指定します。
 *     例えば、1000(ms)を指定すると、1秒ずつ間を空けてAPIを呼び出します。
 *   mw.libs.storedMarkAdmins.apiMaxTry:
 *     権限毎のAPIの呼び出し回数の上限です。
 *     1回あたり500人まで取得できるため、10で管理者が5000人まで動作します。
 *     小さすぎると、動作がおかしくなります。
 *   mw.libs.storedMarkAdmins.apiNotifyMessage:
 *     API呼び出し時にメッセージを表示しますが、そのメッセージを指定します。
 *     '$role'を権限名に置き換えます。
 *     nullを指定することで、メッセージを表示しないことができます。
 * 
 * Local storage:
 *   storageKeyName で指定されたLocal storageを使用します。
 * 
 * このファイルはパブリックドメインとします。
 * This file is public domain.
 */

(function () {
  'use strict';

  /*
   * =============================================================================
   * Settings
   * =============================================================================
   */

  var rolesMap = {
    'sysop': {name: '管理者', abbr: '(管)', global: false },
    'bureaucrat': {name: 'ビューロクラット', abbr: '(ビ)', global: false },
    'steward': {name: 'スチュワード', abbr: '(ス)', global: true },
    'checkuser': {name: 'チェックユーザー', abbr: '(CU)', global: false },
    'ombuds': {name: 'オンブズマン', abbr: '(オ)', global: true },
    'eliminator': {name: '削除者', abbr: '(削)', global: false },
    'rollbacker': {name: '巻き戻し者', abbr: '(巻)', global: false },
    'interface-admin': {name: 'インターフェース管理者', abbr: '(イ)', global: false },
    'abusefilter': {name: '編集フィルター編集者', abbr: '(フ)', global: false },
  };

  var storageKeyName = 'mwStoredMarkAdmins-usersInRole';

  var userNamespaceId = 2;


  /*
   * =============================================================================
   * Utilities
   * =============================================================================
   */

  // try to parse json as JSON
  // returns object or null if failed
  function tryParseJson(json) {
    if (json) {
      try {
        return JSON.parse(json);
      } catch (e) {
        return null;
      }
    } else {
      return null;
    }
  }

  // execute deferreds in series
  // keys: Array
  // deferredFunc: Function, which has one arguments from each element of keys, and returns deferred object
  // interval: sleeps interval milliseconds if not 0 or null
  // returns deferred object
  //   deferred return value: Object, which keys are parameter keys and values are the values of deferred object deferredFunc returned
  // e.g.
  //   DeferredSeries(['a', 'b'], function (key) {
  //     return $.Deferred().resolve(key).promise();
  //   }, 1000).then(function (result) {
  //     console.log(result);
  //   }); // => {a: 'a', b: 'b'}
  function deferredSeries(keys, deferredFunc, interval) {
    if (keys.length === 0) {
      return $.Deferred().resolve({}).promise();
    } else {
      var key0 = keys[0];
      var keysRest = keys.slice(1);
      return deferredFunc(key0).then(function (result0) {
        var deferred = $.Deferred();
        function resolveRest() {
          deferredSeries(keysRest, deferredFunc, interval).then(function (results) {
            results[key0] = result0;
            deferred.resolve(results);
          });
        }
        if (interval && keysRest.length > 0) {
          setTimeout(function () {
            resolveRest();
          }, interval);
        } else {
          resolveRest();
        }
        return deferred.promise();
      });
    }
  }

  // overwrites obj1
  // deepMerge({a: {b: [2], c: 3}, d: {e: {f: [4, 5]}}, g: 6}, {a: {b: [7], c: 8}, d: {e: {f: [9, 10]}}, h: 11})
  //   => {a: {b: [2, 7], c: 8}, d: {e: {f: [4, 5, 9, 10]}}, g: 6, h: 11}
  function deepMerge(obj1, obj2) {
    $.each(obj2, function (key, value2) {
      if (key in obj1) {
        var value1 = obj1[key];
        if (Array.isArray(value1)) {
          if (Array.isArray(value2)) {
            obj1[key] = value1.concat(value2);
          } else {
            value1.push(value);
          }
        } else if (typeof value1 === 'object') {
          if (typeof value2 === 'object') {
            deepMerge(value1, value2);
          } else {
            obj1[key] = value2;
          }
        } else {
          obj1[key] = value2;
        }
      } else {
        obj1[key] = value2;
      }
    });
    return obj1;
  }


  /*
   * =============================================================================
   * Mediawiki API Utilities
   * =============================================================================
   */

  // iterate getting query api if request returned continue
  // api: mw.Api
  // options: Object, get options
  // maxTry: integer, nullable (default 10), max of iterates count
  // interval: integer, nullable (default 1000), milliseconds to sleep between each query
  // deferred: jQuery.Deferred, nullable
  // currentResult: Object, nullable, current query result
  // returns deferred object
  //   deferred return value: query result (data.query)
  function iterateQuery(api, options, maxTry, interval, deferred, currentResult) {
    if (typeof (maxTry) !== 'number') {
      maxTry = 10;
    }
    interval = interval || 1000;
    deferred = deferred || $.Deferred();
    currentResult = currentResult || {
    };
    if (maxTry === 0) {
      deferred.reject('maxTry is 0');
      return deferred;
    }
    api.get($.extend({
      action: 'query',
    }, options)).done(function (data) {
      currentResult = deepMerge(currentResult, data.query);
      if (data.continue ) {
        setTimeout(function () {
          iterateQuery(api, $.extend(options, data.continue ), maxTry - 1, interval, deferred, currentResult);
        }, interval);
      } else {
        deferred.resolve(currentResult);
      }
    });
    return deferred.promise();
  }

  function getAllUsers(api, augroup, maxTry, interval, aulimit) {
    aulimit = aulimit || 'max';
    var options = {
      list: 'allusers',
      augroup: augroup,
      aulimit: aulimit,
    };
    return iterateQuery(api, options, maxTry, interval).then(function(query){
      return $.Deferred().resolve(query.allusers).promise();
    });
  }

  function getGlobalAllUsers(api, agugroup, maxTry, interval, agulimit) {
    agulimit = agulimit || 'max';
    var options = {
      list: 'globalallusers',
      agugroup: agugroup,
      agulimit: agulimit,
    };
    return iterateQuery(api, options, maxTry, interval).then(function(query){
      return $.Deferred().resolve(query.globalallusers).promise();
    });
  }


  /*
   * =============================================================================
   * Getting users in each role
   * =============================================================================
   */

  var roles = Object.keys(rolesMap);

  function getUsersInRoleFromApi(api, options, callback) {
    deferredSeries(roles, function (role) {
      if (options.apiNotifyMessage) {
        mw.notify(options.apiNotifyMessage.replace('$role', rolesMap[role].name));
      }
      if (rolesMap[role].global) {
        return getGlobalAllUsers(api, role, options.apiMaxTry, options.apiCallInterval);
      } else {
        return getAllUsers(api, role, options.apiMaxTry, options.apiCallInterval);
      }
    }, options.apiCallInterval).done(function (usersInRoleResult) {
      var usersInRole = {};
      $.each(usersInRoleResult, function (role, usersArray) {
        usersInRole[role] = usersArray.map(function (userInfo) { return userInfo.name; });
      });
      callback(usersInRole);
    });
  }

  function usersInRoleToStoredObject(usersInRole) {
    return {
      usersInRole: usersInRole,
      updatedAt: Date.now(),
    };
  }

  function getUsersInRoleFromStore(options) {
    var json = mw.storage.get(storageKeyName);
    var storedObject = tryParseJson(json);
    if (storedObject && storedObject.updatedAt && (Date.now() < storedObject.updatedAt + options.validTermInMs)) {
      return storedObject.usersInRole;
    } else {
      return null;
    }
  }

  function getUsersInRole(api, options) {
    var deferred = $.Deferred();
    var usersInRoleFromStore = getUsersInRoleFromStore(options);
    if (usersInRoleFromStore) {
      deferred.resolve(usersInRoleFromStore);
    } else {
      getUsersInRoleFromApi(api, options, function(usersInRole){
        mw.storage.set(storageKeyName, JSON.stringify(usersInRoleToStoredObject(usersInRole)));
        deferred.resolve(usersInRole);
      });
    }
    return deferred.promise();
  }


  /*
   * =============================================================================
   * Getting users in each role
   * =============================================================================
   */

  function getNamespacesFromId(config, namespaceId) {
    return Object.keys(config.wgNamespaceIds).filter(function (id) {
      return config.wgNamespaceIds[id] === namespaceId;
    });
  }

  function getArticleRegexp(config) {
    return new RegExp('^' + mw.RegExp.escape(config.wgArticlePath).replace(mw.RegExp.escape('$1'), '(.+)') + '$');
  }

  function usersInRoleToPageNames(usersInRole, userNamespaces) {
    var pageNamesInRole = {};
    $.each(usersInRole, function (role, users) {
      // used as flat map
      pageNamesInRole[role] = $.map(userNamespaces, function (namespace) {
        return $.map(users, function (user) { return namespace + ':' + user; });
      });
    });
    return pageNamesInRole;
  }

  function getNarrowingDownRegexpFromPageNamesInRole(pageNamesInRole) {
    var mergedPageNames = $.uniqueSort($.map(Object.keys(pageNamesInRole), function (role) {
      return pageNamesInRole[role];
    }));
    return new RegExp(mergedPageNames.map(function (pageName) {
      return mw.RegExp.escape(mw.util.wikiUrlencode(pageName));
    }).join('|'), 'i');
  }

  function markUsers(usersInRole, config, userNamespaceId) {
    var userNamespaces = getNamespacesFromId(config, userNamespaceId);
    var pageNamesInRole = usersInRoleToPageNames(usersInRole, userNamespaces);
    var narrowingDownRegexp = getNarrowingDownRegexpFromPageNamesInRole(pageNamesInRole);
    var articleRegexp = getArticleRegexp(config);
    mw.util.$content.find('a[href]').filter(function () {
      return narrowingDownRegexp.test($(this).attr('href'));
    }).each(function () {
      var $link = $(this);
      var href = $link.attr('href');
      var articleMatch = new RegExp(articleRegexp).exec(href);
      var articleName = (articleMatch && articleMatch[1]) || (new mw.Uri(href)).query.title;
      if (articleName) {
        var title = new mw.Title(decodeURI(articleName));
        if (title.getNamespaceId() === userNamespaceId) {
          var userText = title.getMainText();
          $.each(usersInRole, function (role, users) {
            if (users.indexOf(userText) != - 1) {
              $('<span>').addClass('mark-admins mark-admins-' + role).text(rolesMap[role].abbr).appendTo($link);
            }
          });
        }
      }
    });
  }


  function main(config, options) {
    var api = new mw.Api();
    getUsersInRole(api, options).then(function (usersInRole) {
      markUsers(usersInRole, config, userNamespaceId);
    });
    
    mw.util.addCSS(
      '.mark-admins{ padding-left: 1ex; font-weight: bold; }\n' +
      ''
    );
  }

  $(function () {
    if (!('storedMarkAdmins' in mw.libs)) {
      mw.libs.storedMarkAdmins = {
        validTermInMs: 30 * 24 * 60 * 60 * 1000, // 30 days
        apiCallInterval: 1000, // 1000ms
        apiMaxTry: 10, // up to 5000 users for each role
        apiNotifyMessage: '$role一覧を取得中…', // substitute '$role' to role name, null to not to show notify
      };
    }

    mw.loader.using([ 'mediawiki.api', 'mediawiki.util', 'mediawiki.storage',
      'mediawiki.Title', 'mediawiki.Uri', 'mediawiki.util']).then(function () {
      var config = mw.config.get(['wgAction', 'wgNamespaceNumber', 'wgNamespaceIds', 'wgArticlePath']);
      var isArticleView = (config.wgAction === 'view') && (config.wgNamespaceNumber === 0);
      if ((!isArticleView) && (config.wgAction !== 'edit')) {
        main(config, mw.libs.storedMarkAdmins);
      }
    });
  });
}) ();