MediaWiki:Gadget-abuseLogRC-core.js

After saving, you may need to bypass your browser's cache to see the changes. For further information, see Wikipedia:Bypass your cache.

  • In most Windows and Linux browsers: Hold down Ctrl and press F5.
  • In Safari: Hold down ⇧ Shift and click the Reload button.
  • In Chrome and Firefox for Mac: Hold down both ⌘ Cmd+⇧ Shift and press R.
"use strict";

function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
function _toConsumableArray(r) { return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread(); }
function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
function _iterableToArray(r) { if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r); }
function _arrayWithoutHoles(r) { if (Array.isArray(r)) return _arrayLikeToArray(r); }
function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; }
function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t["return"] || t["return"](); } finally { if (u) throw o; } } }; }
function _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); }
function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t["return"] && (u = t["return"](), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
function _arrayWithHoles(r) { if (Array.isArray(r)) return r; }
/*  ======================
	      AbuseLogRC
	======================

	Shows certain Special:AbuseLog entries at the top of Special:RecentChanges
	for better vandalism detection. Use of this gadget requires the user right
	to view private filters ("abusefilter-log-private").

	Keep this in sync with [[rsw:MediaWiki:Gadget-abuseLogRC-core.js]].

	Inspired by Suppa chuppa's original script at [[User:Suppa chuppa/abuselog.js]]

	@author Iiii_I_I_I
*/

;
(function ($, mw) {
  var gadgetLoaded = false;
  var entryDays = new Set();
  var lastUpdate;
  var intervalID;
  var filters = '2|3|5|6|7|12|14|19|21|global-4';

  // default config
  if (getConfig('autoRefresh') === null) setConfig('autoRefresh', false);
  if (getConfig('interval') === null) setConfig('interval', 30);
  if (getConfig('entries') === null) setConfig('entries', 5);
  function getConfig(key) {
    return JSON.parse(localStorage.getItem('gadget-abuseLogRC-' + key));
  }
  function setConfig(key, value) {
    localStorage.setItem('gadget-abuseLogRC-' + key, value);
  }
  function refreshData() {
    $('.gadget-abuselog-list').addClass('loading'); // class is cleared when new list replaces old
    entryDays.clear();
    getData();
  }
  function toggleAutoRefresh(isToggledOn, refreshButton) {
    if (isToggledOn) {
      intervalID = setInterval(refreshData, getConfig('interval') * 1000);
    } else {
      clearInterval(intervalID);
    }

    // hide manual refresh button when autoRefresh is on, show when it's off
    refreshButton.toggle(!isToggledOn);

    // update cookie
    setConfig('autoRefresh', isToggledOn);
  }
  function buildGadget(_ref) {
    var _ref2 = _slicedToArray(_ref, 3),
      entries = _ref2[0],
      users = _ref2[1],
      pages = _ref2[2];
    lastUpdate = new Date();

    // refreshed
    if (gadgetLoaded) {
      $('.gadget-abuselog-list').replaceWith(buildList(entries, users, pages));
    }
    // initial load
    else {
      var $container = $('<div class="gadget-abuselog"></div>');
      $container.append(buildHeader(), buildList(entries, users, pages));
      $('.mw-changeslist').before($container);
      gadgetLoaded = true;

      // change user tool link text if readableRC is on
      mw.hook('ext.gadget.readableRC').add(function () {
        $('.gadget-abuselog').addClass('match-gadget-rc');
      });
    }
    mw.hook('ext.gadget.abuseLogRC').fire();
  }
  function buildHeader() {
    var $header = $('<div class="gadget-abuselog-header"></div>');
    var $left = $('<span class="gadget-abuselog-header-left"></span>');
    var $right = buildSettings();
    var $title = $('<h4>Abuse log</h4>');
    var link = ' (' + buildLink('Special:AbuseLog', {
      exists: true,
      text: 'all'
    }) + ')';
    $left.append($title, link);
    $header.append($left, $right);
    return $header;
  }
  function buildSettings() {
    var $settings = $('<span class="gadget-abuselog-header-right"></span>');
    var refreshButton = new OO.ui.ButtonWidget({
      framed: false,
      icon: 'reload',
      flags: ['progressive'],
      label: 'Refresh log',
      invisibleLabel: true,
      title: 'Refresh abuse log entries',
      classes: ['gadget-abuselog-manual-refresh']
    });

    // hide when autoRefresh is on, show when it's off
    refreshButton.toggle(!getConfig('autoRefresh'));
    refreshButton.on('click', function (e) {
      refreshButton.setDisabled(true); // disabled state cleared by hook below
      refreshData();
    });

    // use RecentChanges' "View new changes" button as another way to refresh
    $('.mw-rcfilters-ui-filterWrapperWidget-showNewChanges a').on('click', function (e) {
      refreshData();
    });

    // POPUP TOP HALF: number of log entries to show
    var entriesSelectWidget = new OO.ui.ButtonSelectWidget({
      items: [new OO.ui.ButtonOptionWidget({
        data: '3',
        label: '3'
      }), new OO.ui.ButtonOptionWidget({
        data: '5',
        label: '5'
      }), new OO.ui.ButtonOptionWidget({
        data: '10',
        label: '10'
      }), new OO.ui.ButtonOptionWidget({
        data: '20',
        label: '20'
      }), new OO.ui.ButtonOptionWidget({
        data: '50',
        label: '50'
      })]
    });
    var entriesFieldset = new OO.ui.FieldsetLayout({
      label: 'Entries to show',
      classes: ['gadget-abuselog-settings-entries'],
      items: [entriesSelectWidget]
    });
    entriesSelectWidget.selectItemByData(getConfig('entries').toString());
    entriesSelectWidget.on('choose', function (button, selected) {
      setConfig('entries', button.data);
      refreshData();
    });

    // if user changes # entries to a custom value in localStorage, insert new button at the start
    if (entriesSelectWidget.findSelectedItem() === null) {
      var value = getConfig('entries').toString();
      var customButton = new OO.ui.ButtonOptionWidget({
        data: value,
        label: value
      });
      entriesSelectWidget.addItems(customButton, 0);
      entriesSelectWidget.selectItem(customButton);
    }

    // POPUP BOTTOM HALF: refresh settings
    var lastUpdatedLabel = new OO.ui.LabelWidget({
      classes: ['gadget-abuselog-settings-last-updated']
    });
    var autoRefreshCheckbox = new OO.ui.CheckboxInputWidget({
      selected: getConfig('autoRefresh')
    });
    var refreshFieldset = new OO.ui.FieldsetLayout({
      label: 'Refresh',
      classes: ['gadget-abuselog-settings-refresh'],
      items: [lastUpdatedLabel, new OO.ui.FieldLayout(autoRefreshCheckbox, {
        classes: ['gadget-abuselog-settings-auto-refresh'],
        label: 'Auto-refresh log entries every ' + getConfig('interval') + ' seconds'
      })]
    });

    // initial load
    toggleAutoRefresh(getConfig('autoRefresh'), refreshButton);

    // when clicked
    autoRefreshCheckbox.on('change', function (isSelected, indeterminate) {
      toggleAutoRefresh(isSelected, refreshButton);
    });

    // POPUP MENU
    var menuButton = new OO.ui.PopupButtonWidget({
      icon: 'menu',
      framed: false,
      label: 'Abuse log settings',
      invisibleLabel: true,
      classes: ['gadget-abuselog-settings'],
      popup: {
        head: false,
        anchor: false,
        padded: true,
        autoFlip: false,
        align: 'backwards',
        $content: $('<div>').append(entriesFieldset.$element, refreshFieldset.$element)
      }
    });

    // to do when settings popup is opened
    menuButton.on('click', function () {
      var time = new Intl.DateTimeFormat('en-GB', {
        hour: 'numeric',
        minute: 'numeric',
        timeZone: 'UTC'
      }).format(lastUpdate);
      var day = new Intl.DateTimeFormat('en-GB', {
        day: 'numeric',
        month: 'long',
        year: 'numeric',
        timeZone: 'UTC'
      }).format(lastUpdate);
      var diff = new Date() - lastUpdate;
      var h = Math.floor(diff / 1000 / 60 / 60);
      var m = Math.floor(diff / 1000 / 60) % 60;
      var s = Math.floor(diff / 1000) % 60;

      // if over one minute since last update, hide seconds unit;
      // if under one hour since last update, hide hour unit
      var hh = h > 0 ? h + 'h ' : '';
      var mm = m + 'm';
      var ss = s + 's';
      var ago = h > 0 || m > 0 ? hh + mm : ss;
      lastUpdatedLabel.setLabel(new OO.ui.HtmlSnippet("Last update: <strong class=\"last-update\" title=\"".concat(time, ", ").concat(day, "\">").concat(ago, " ago</strong>.")));
    });

    // to do on each gadget refresh
    mw.hook('ext.gadget.abuseLogRC').add(function () {
      refreshButton.setDisabled(false);
    });
    $settings.append(refreshButton.$element, menuButton.$element);
    return $settings;
  }
  function buildList(entries, users, pages) {
    var $list = $('<div class="gadget-abuselog-list"></div>');
    var pageArr = Object.values(pages);
    var _iterator = _createForOfIteratorHelper(entries),
      _step;
    try {
      var _loop = function _loop() {
        var entry = _step.value;
        var user = users.find(function (user) {
          return user.name === entry.user;
        });
        var page = pageArr.find(function (page) {
          return page.title === entry.title;
        });
        var userPage = pageArr.find(function (page) {
          return page.title === "User:".concat(user.name);
        });
        var talkPage = pageArr.find(function (page) {
          return page.title === "User talk:".concat(user.name);
        });

        // prevent error; User:FeedbackBot redirects to RuneScape:Article feedback
        if (user.name === 'FeedbackBot') {
          userPage = pageArr.find(function (page) {
            return page.title === 'RuneScape:Article feedback';
          });
        }
        var opts = {
          isRegistered: Object.hasOwn(user, 'userid'),
          isRedirect: page === undefined,
          // API response separates redirects from pages object
          pageExists: page === undefined || Object.hasOwn(page, 'pageid'),
          userPageExists: Object.hasOwn(userPage, 'pageid'),
          talkPageExists: Object.hasOwn(talkPage, 'pageid')
        };
        $list.append(buildRow(entry, opts));
      };
      for (_iterator.s(); !(_step = _iterator.n()).done;) {
        _loop();
      }
    } catch (err) {
      _iterator.e(err);
    } finally {
      _iterator.f();
    }
    return $list;
  }
  function buildRow(entry, opts) {
    // FIRST COLUMN: date and time
    var entryDate = new Date(entry.timestamp);
    var entryDay = new Intl.DateTimeFormat('en-GB', {
      day: '2-digit',
      month: 'short',
      timeZone: 'UTC'
    }).format(entryDate);
    var entryTime = new Intl.DateTimeFormat('en-GB', {
      hour: 'numeric',
      minute: 'numeric',
      timeZone: 'UTC'
    }).format(entryDate);
    var showHideDay = entryDays.has(entryDay) ? 'hide' : '';
    var firstColumn = '<span class="gadget-abuselog-col gadget-abuselog-col-1">' + "<span class=\"".concat(showHideDay, "\">").concat(entryDay, "</span> <span>").concat(entryTime, "</span>") + '</span>';
    entryDays.add(entryDay);

    // SECOND COLUMN: page edited
    var secondColumn = '<span class="gadget-abuselog-col gadget-abuselog-col-2">' + buildLink(entry.title, {
      exists: opts.pageExists,
      text: entry.title,
      isRedirect: opts.isRedirect
    }) + '</span>';

    // THIRD COLUMN: diff, details, and action taken
    var diffLink = entry.revid ? buildLink("Special:Diff/".concat(entry.revid), {
      exists: true,
      text: 'diff'
    }) : 'diff';
    var logLink = buildLink("Special:AbuseLog/".concat(entry.id), {
      exists: true,
      text: 'log'
    });
    var results = [];

    // some filters perform multiple actions on a single edit, eg. <https://oldschool.runescape.wiki/w/Special:AbuseLog/22296>
    entry.result.split(',').forEach(function (result) {
      results.push("<span class=\"gadget-abuselog-result gadget-abuselog-result-".concat(result, "\">") + "".concat(mw.msg('abusefilter-action-' + result), "</span>"));
    });
    var thirdColumn = '<span class="gadget-abuselog-col gadget-abuselog-col-3">' + "(".concat(diffLink, " | ").concat(logLink, ") <span class=\"gadget-abuselog-action\">(").concat(results.join(', '), ")</span>") + '</span>';

    // FOURTH COLUMN: user details and filter triggered
    var filterURL = "Special:AbuseFilter/".concat(entry.filter_id);
    if (entry.filter_id.includes('global')) {
      filterURL = "meta:Special:AbuseFilter/".concat(entry.filter_id.replace('global-', ''));
    }
    var filterLink = buildLink(filterURL, {
      exists: true,
      text: "Filter ".concat(entry.filter_id)
    });
    var fourthColumn = '<span class="gadget-abuselog-col gadget-abuselog-col-4">' + buildUserLinks(entry.user, opts) + "<span class=\"gadget-abuselog-filter gadget-abuselog-filter-".concat(entry.filter_id, "\">") + "(".concat(filterLink, ": ").concat(entry.filter, ")") + '</span> ' + '</span>';
    return '<div class="gadget-abuselog-row">' + firstColumn + secondColumn + thirdColumn + fourthColumn + '</div>';
  }
  function buildUserLinks(username, opts) {
    var userLink;
    var toolLinks;

    // link text is added with CSS so it can be replaced when readableRC is loaded
    var talkLink = "<span>".concat(buildLink("User talk:".concat(username), {
      exists: opts.talkPageExists,
      text: '',
      classes: 'mw-usertoollinks-talk'
    }), "</span>");
    var contribsLink = "<span>".concat(buildLink("Special:Contributions/".concat(username), {
      exists: true,
      text: '',
      classes: 'mw-usertoollinks-contribs'
    }), "</span>");
    var logLink = "<span>".concat(buildLink("Special:AbuseLog", {
      exists: true,
      text: '',
      classes: 'mw-usertoollinks-abuselog',
      param: 'wpSearchUser',
      value: username
    }), "</span>");
    var blockLink = "<span>".concat(buildLink("Special:Block/".concat(username), {
      exists: true,
      text: '',
      classes: 'mw-usertoollinks-block'
    }), "</span>");

    // user links vs. anon links
    if (opts.isRegistered) {
      userLink = "<span>".concat(buildLink("User:".concat(username), {
        exists: opts.userPageExists,
        text: username,
        classes: 'mw-userlink'
      }), "</span>");
      toolLinks = '<span class="mw-usertoollinks">(' + talkLink + contribsLink + logLink + blockLink + ')</span>';
    } else {
      userLink = "<span>".concat(buildLink("Special:Contributions/".concat(username), {
        exists: true,
        text: username,
        classes: 'mw-userlink mw-anonuserlink'
      }), "</span>");
      toolLinks = '<span class="mw-usertoollinks">(' + talkLink + logLink + blockLink + ')</span>';
    }
    return userLink + ' ' + toolLinks + ' ';
  }
  function buildLink(pagename, opts) {
    var url = mw.util.getUrl(pagename);
    if (opts.param) url = mw.util.getUrl(pagename, _defineProperty({}, opts.param, opts.value));
    if (!opts.exists) url = mw.util.getUrl(pagename, {
      action: 'edit'
    });
    if (opts.isRedirect) url = mw.util.getUrl(pagename, {
      redirect: 'no'
    });
    var title = opts.exists ? pagename : pagename + ' (page does not exist)';
    var redlink = opts.exists ? '' : 'new';
    var classes = opts.classes || '';
    var link = "<a href=\"".concat(url, "\" title=\"").concat(title, "\" class=\"").concat(redlink, " ").concat(classes, "\">").concat(opts.text, "</a>");
    return link;
  }
  function getUsernames(abuselog) {
    var usernames = new Set();
    var _iterator2 = _createForOfIteratorHelper(abuselog),
      _step2;
    try {
      for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
        var entry = _step2.value;
        usernames.add(entry.user);
      }
    } catch (err) {
      _iterator2.e(err);
    } finally {
      _iterator2.f();
    }
    return _toConsumableArray(usernames);
  }
  function getPageTitles(abuselog) {
    var pages = new Set();
    var usernames = getUsernames(abuselog);
    var _iterator3 = _createForOfIteratorHelper(abuselog),
      _step3;
    try {
      for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
        var entry = _step3.value;
        pages.add(entry.title);
      }
    } catch (err) {
      _iterator3.e(err);
    } finally {
      _iterator3.f();
    }
    var _iterator4 = _createForOfIteratorHelper(usernames),
      _step4;
    try {
      for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
        var username = _step4.value;
        pages.add('User:' + username);
        pages.add('User talk:' + username);
      }
    } catch (err) {
      _iterator4.e(err);
    } finally {
      _iterator4.f();
    }
    return _toConsumableArray(pages);
  }
  function setMessages(messages) {
    var _iterator5 = _createForOfIteratorHelper(messages),
      _step5;
    try {
      for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) {
        var message = _step5.value;
        mw.messages.set(message['name'], message['*']);
      }
    } catch (err) {
      _iterator5.e(err);
    } finally {
      _iterator5.f();
    }
  }
  function buildError(result) {
    var warning = new OO.ui.MessageWidget({
      type: 'notice',
      classes: ['gadget-abuselog-error'],
      label: new OO.ui.HtmlSnippet('<strong>AbuseLogRC encountered an API error:</strong><br>' + result.error.info)
    });
    $('.mw-changeslist').before(warning.$element);
  }
  function getData() {
    var api = new mw.Api();

    // this api call gets:
    // - abuselog entries for commonly triggered vandalism filters
    // - mw messages for abusefilter results (tag, warn, disallow, etc.)
    api.get({
      list: 'abuselog',
      afllimit: getConfig('entries'),
      aflprop: 'ids|user|title|action|result|timestamp|revid|filter',
      aflfilter: filters,
      meta: 'allmessages',
      amprefix: 'abusefilter-action-'
    }).then(function (results) {
      var abuselogResult = results.query.abuselog;
      var messagesResult = results.query.allmessages;
      if (!gadgetLoaded) setMessages(messagesResult);

      // this api call gets:
      // - page info for target pages in abuselog
      // - user info for users listed in abuselog
      // - page info for those users' userpages and talk pages
      return api.get({
        list: 'users',
        ususers: getUsernames(abuselogResult),
        titles: getPageTitles(abuselogResult),
        redirects: true
      }).then(
      // success
      function (results) {
        var usersResult = results.query.users;
        var pagesResult = results.query.pages;
        return [abuselogResult, usersResult, pagesResult];
      },
      // fail
      function (code, result) {
        buildError(result);
      });
    }).then(
    // success
    function (results) {
      buildGadget(results);
    },
    // fail
    function (code, result) {
      buildError(result);
    });
  }
  getData();
})(jQuery, mediaWiki);