MediaWiki:Gadget-abuseLogRC-core.js: Difference between revisions

 
No edit summary
 
(3 intermediate revisions by the same user not shown)
Line 1: Line 1:
"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
AbuseLogRC
Line 14: Line 31:
*/
*/


;
;(function ($, mw) {
(function ($, mw) {
let gadgetLoaded = false;
var gadgetLoaded = false;
let entryDays = new Set();
var entryDays = new Set();
let lastUpdate;
var lastUpdate;
let intervalID;
var intervalID;
let filters = '2|3|5|6|7|12|14|19|21|global-4';
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([entries, users, pages]) {
lastUpdate = new Date();

// refreshed
if (gadgetLoaded) {
$('.gadget-abuselog-list').replaceWith(buildList(entries, users, pages));
}
// initial load
else {
let $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() {
let $header = $('<div class="gadget-abuselog-header"></div>');
let $left = $('<span class="gadget-abuselog-header-left"></span>');
let $right = buildSettings();
let $title = $('<h4>Abuse log</h4>');
let link = ' (' + buildLink('Special:AbuseLog', {exists: true, text: 'all'}) + ')';

$left.append($title, link);
$header.append($left, $right);

return $header;
}

function buildSettings() {
let $settings = $('<span class="gadget-abuselog-header-right"></span>');
let 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
let 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' })
]
});
let 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) {
let value = getConfig('entries').toString();
let customButton = new OO.ui.ButtonOptionWidget({ data: value, label: value });

entriesSelectWidget.addItems(customButton, 0);
entriesSelectWidget.selectItem(customButton);
}

// POPUP BOTTOM HALF: refresh settings
let lastUpdatedLabel = new OO.ui.LabelWidget({
classes: ['gadget-abuselog-settings-last-updated']
});
let autoRefreshCheckbox = new OO.ui.CheckboxInputWidget({
selected: getConfig('autoRefresh')
});
let 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
let 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 () {
let time = new Intl.DateTimeFormat('en-GB', {hour: 'numeric', minute: 'numeric', timeZone: 'UTC'}).format(lastUpdate);
let day = new Intl.DateTimeFormat('en-GB', {day: 'numeric', month: 'long', year: 'numeric', timeZone: 'UTC'}).format(lastUpdate);

let diff = new Date() - lastUpdate;
let h = Math.floor(diff / 1000 / 60 / 60);
let m = Math.floor(diff / 1000 / 60) % 60;
let 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
let hh = (h > 0) ? h + 'h ' : '';
let mm = m + 'm';
let ss = s + 's';
let ago = (h > 0 || m > 0) ? hh + mm : ss;

lastUpdatedLabel.setLabel(
new OO.ui.HtmlSnippet(`Last update: <strong class="last-update" title="${time}, ${day}">${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) {
let $list = $('<div class="gadget-abuselog-list"></div>');
let pageArr = Object.values(pages);

for (let entry of entries) {
let user = users.find(user => user.name === entry.user);
let page = pageArr.find(page => page.title === entry.title);
let userPage = pageArr.find(page => page.title === `User:${user.name}`);
let talkPage = pageArr.find(page => page.title === `User talk:${user.name}`);

// prevent error; User:FeedbackBot redirects to RuneScape:Article feedback
if (user.name === 'FeedbackBot') {
userPage = pageArr.find(page => page.title === 'RuneScape:Article feedback');
}

let 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));
}

return $list;
}

function buildRow(entry, opts) {
// FIRST COLUMN: date and time
let entryDate = new Date(entry.timestamp);
let entryDay = new Intl.DateTimeFormat('en-GB', {day: '2-digit', month: 'short', timeZone: 'UTC'}).format(entryDate);
let entryTime = new Intl.DateTimeFormat('en-GB', {hour: 'numeric', minute: 'numeric', timeZone: 'UTC'}).format(entryDate);
let showHideDay = (entryDays.has(entryDay)) ? 'hide' : '';
let firstColumn =
'<span class="gadget-abuselog-col gadget-abuselog-col-1">' +
`<span class="${showHideDay}">${entryDay}</span> <span>${entryTime}</span>` +
'</span>';

entryDays.add(entryDay);

// SECOND COLUMN: page edited
let 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
let diffLink = (entry.revid) ? buildLink(`Special:Diff/${entry.revid}`, {exists: true, text: 'diff'}) : 'diff';
let logLink = buildLink(`Special:AbuseLog/${entry.id}`, {exists: true, text: 'log'});
let results = [];

// some filters perform multiple actions on a single edit, eg. <https://oldschool.runescape.wiki/w/Special:AbuseLog/22296>
entry.result.split(',').forEach(result => {
results.push(
`<span class="gadget-abuselog-result gadget-abuselog-result-${result}">` +
`${mw.msg('abusefilter-action-' + result)}</span>`
);
});

let thirdColumn =
'<span class="gadget-abuselog-col gadget-abuselog-col-3">' +
`(${diffLink} | ${logLink}) <span class="gadget-abuselog-action">(${results.join(', ')})</span>` +
'</span>';

// FOURTH COLUMN: user details and filter triggered
let filterURL = `Special:AbuseFilter/${entry.filter_id}`;

if (entry.filter_id.includes('global')) {
filterURL = `meta:Special:AbuseFilter/${entry.filter_id.replace('global-', '')}`;
}

let filterLink = buildLink(filterURL, {exists: true, text: `Filter ${entry.filter_id}`});

let fourthColumn =
'<span class="gadget-abuselog-col gadget-abuselog-col-4">' +
buildUserLinks(entry.user, opts) +
`<span class="gadget-abuselog-filter gadget-abuselog-filter-${entry.filter_id}">` +
`(${filterLink}: ${entry.filter})` +
'</span> ' +
'</span>';


// default config
return '<div class="gadget-abuselog-row">' + firstColumn + secondColumn + thirdColumn + fourthColumn + '</div>';
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
function buildUserLinks(username, opts) {
refreshButton.toggle(!isToggledOn);
let userLink;
let toolLinks;


// update cookie
// link text is added with CSS so it can be replaced when readableRC is loaded
setConfig('autoRefresh', isToggledOn);
let talkLink = `<span>${buildLink(`User talk:${username}`, {
}
exists: opts.talkPageExists,
function buildGadget(_ref) {
text: '',
var _ref2 = _slicedToArray(_ref, 3),
classes: 'mw-usertoollinks-talk'
entries = _ref2[0],
})}</span>`;
users = _ref2[1],
let contribsLink = `<span>${buildLink(`Special:Contributions/${username}`, {
pages = _ref2[2];
exists: true,
lastUpdate = new Date();
text: '',
classes: 'mw-usertoollinks-contribs'
})}</span>`;
let logLink = `<span>${buildLink(`Special:AbuseLog`, {
exists: true,
text: '',
classes: 'mw-usertoollinks-abuselog',
param: 'wpSearchUser',
value: username
})}</span>`;
let blockLink = `<span>${buildLink(`Special:Block/${username}`, {
exists: true,
text: '',
classes: 'mw-usertoollinks-block'
})}</span>`;


// refreshed
// user links vs. anon links
if (opts.isRegistered) {
if (gadgetLoaded) {
$('.gadget-abuselog-list').replaceWith(buildList(entries, users, pages));
userLink = `<span>${buildLink(`User:${username}`, {
}
exists: opts.userPageExists,
// initial load
text: username,
else {
classes: 'mw-userlink'
var $container = $('<div class="gadget-abuselog"></div>');
})}</span>`;
$container.append(buildHeader(), buildList(entries, users, pages));
toolLinks = '<span class="mw-usertoollinks">(' + talkLink + contribsLink + logLink + blockLink + ')</span>';
$('.mw-changeslist').before($container);
} else {
gadgetLoaded = true;
userLink = `<span>${buildLink(`Special:Contributions/${username}`, {
exists: true,
text: username,
classes: 'mw-userlink mw-anonuserlink'
})}</span>`;
toolLinks = '<span class="mw-usertoollinks">(' + talkLink + logLink + blockLink + ')</span>';
}


// change user tool link text if readableRC is on
return userLink + ' ' + toolLinks + ' ';
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
function buildLink(pagename, opts) {
refreshButton.toggle(!getConfig('autoRefresh'));
let url = mw.util.getUrl(pagename);
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
if (opts.param) url = mw.util.getUrl(pagename, {[opts.param]: opts.value});
$('.mw-rcfilters-ui-filterWrapperWidget-showNewChanges a').on('click', function (e) {
if (!opts.exists) url = mw.util.getUrl(pagename, {action: 'edit'});
refreshData();
if (opts.isRedirect) url = mw.util.getUrl(pagename, {redirect: 'no'});
});


// POPUP TOP HALF: number of log entries to show
let title = (opts.exists) ? pagename : pagename + ' (page does not exist)';
var entriesSelectWidget = new OO.ui.ButtonSelectWidget({
let redlink = (opts.exists) ? '' : 'new';
items: [new OO.ui.ButtonOptionWidget({
let classes = opts.classes || '';
data: '3',
let link = `<a href="${url}" title="${title}" class="${redlink} ${classes}">${opts.text}</a>`;
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
return link;
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
function getUsernames(abuselog) {
var lastUpdatedLabel = new OO.ui.LabelWidget({
let usernames = new Set();
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
for (let entry of abuselog) {
toggleAutoRefresh(getConfig('autoRefresh'), refreshButton);
usernames.add(entry.user);
}


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


// POPUP MENU
function getPageTitles(abuselog) {
var menuButton = new OO.ui.PopupButtonWidget({
let pages = new Set();
icon: 'menu',
let usernames = getUsernames(abuselog);
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
for (let entry of abuselog) {
menuButton.on('click', function () {
pages.add(entry.title);
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;
for (let username of usernames) {
// if under one hour since last update, hide hour unit
pages.add('User:' + username);
var hh = h > 0 ? h + 'h ' : '';
pages.add('User talk:' + username);
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
return [...pages];
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
function setMessages(messages) {
if (user.name === 'FeedbackBot') {
for (let message of messages) {
userPage = pageArr.find(function (page) {
mw.messages.set(message['name'], message['*']);
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
function buildError(result) {
var secondColumn = '<span class="gadget-abuselog-col gadget-abuselog-col-2">' + buildLink(entry.title, {
let warning = new OO.ui.MessageWidget({
exists: opts.pageExists,
type: 'notice',
text: entry.title,
classes: ['gadget-abuselog-error'],
isRedirect: opts.isRedirect
label: new OO.ui.HtmlSnippet('<strong>AbuseLogRC encountered an API error:</strong><br>' + result.error.info)
}) + '</span>';
});


// THIRD COLUMN: diff, details, and action taken
$('.mw-changeslist').before(warning.$element);
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>
function getData() {
entry.result.split(',').forEach(function (result) {
let api = new mw.Api();
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
// this api call gets:
var filterURL = "Special:AbuseFilter/".concat(entry.filter_id);
// - abuselog entries for commonly triggered vandalism filters
if (entry.filter_id.includes('global')) {
// - mw messages for abusefilter results (tag, warn, disallow, etc.)
filterURL = "meta:Special:AbuseFilter/".concat(entry.filter_id.replace('global-', ''));
api.get({
}
list: 'abuselog',
var filterLink = buildLink(filterURL, {
afllimit: getConfig('entries'),
exists: true,
aflprop: 'ids|user|title|action|result|timestamp|revid|filter',
text: "Filter ".concat(entry.filter_id)
aflfilter: filters,
});
meta: 'allmessages',
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>';
amprefix: 'abusefilter-action-'
return '<div class="gadget-abuselog-row">' + firstColumn + secondColumn + thirdColumn + fourthColumn + '</div>';
})
}
.then(function (results) {
function buildUserLinks(username, opts) {
let abuselogResult = results.query.abuselog;
var userLink;
let messagesResult = results.query.allmessages;
var toolLinks;


// link text is added with CSS so it can be replaced when readableRC is loaded
if (!gadgetLoaded) setMessages(messagesResult);
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
// this api call gets:
if (opts.isRegistered) {
// - page info for target pages in abuselog
userLink = "<span>".concat(buildLink("User:".concat(username), {
// - user info for users listed in abuselog
exists: opts.userPageExists,
// - page info for those users' userpages and talk pages
text: username,
return api.get({
classes: 'mw-userlink'
list: 'users',
}), "</span>");
ususers: getUsernames(abuselogResult),
toolLinks = '<span class="mw-usertoollinks">(' + talkLink + contribsLink + logLink + blockLink + ')</span>';
titles: getPageTitles(abuselogResult),
} else {
redirects: true
userLink = "<span>".concat(buildLink("Special:Contributions/".concat(username), {
})
exists: true,
.then(
text: username,
// success
classes: 'mw-userlink mw-anonuserlink'
function (results) {
}), "</span>");
let usersResult = results.query.users;
toolLinks = '<span class="mw-usertoollinks">(' + talkLink + logLink + blockLink + ')</span>';
let pagesResult = results.query.pages;
}
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:
return [abuselogResult, usersResult, pagesResult];
// - abuselog entries for commonly triggered vandalism filters
},
// - mw messages for abusefilter results (tag, warn, disallow, etc.)
// fail
api.get({
function (code, result) {
list: 'abuselog',
buildError(result);
afllimit: getConfig('entries'),
}
aflprop: 'ids|user|title|action|result|timestamp|revid|filter',
);
aflfilter: filters,
})
meta: 'allmessages',
.then(
amprefix: 'abusefilter-action-'
// success
function (results) {
}).then(function (results) {
var abuselogResult = results.query.abuselog;
buildGadget(results);
var messagesResult = results.query.allmessages;
},
if (!gadgetLoaded) setMessages(messagesResult);
// fail
function (code, result) {
buildError(result);
}
);
}


// this api call gets:
getData();
// - page info for target pages in abuselog
}(jQuery, mediaWiki));
// - 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);

Latest revision as of 12:06, 20 October 2024

"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);