MediaWiki:Gadget-abuseLogRC-core.js
Jump to navigation
Jump to search
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);