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 (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 |
|||
}).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);