(Added Spam Obliteration and Prevention members.) |
m (Now I see why I should have waited for the VSTF usergroup to become deprecated.) |
||
Line 73: | Line 73: | ||
'sysop', |
'sysop', |
||
'helper', |
'helper', |
||
− | 'vstf', |
||
'staff', |
'staff', |
||
'global-discussions-moderator', |
'global-discussions-moderator', |
||
Line 93: | Line 92: | ||
'helper', |
'helper', |
||
'staff', |
'staff', |
||
− | 'vstf', |
||
'content-team-member', |
'content-team-member', |
||
'wiki-manager', |
'wiki-manager', |
||
Line 99: | Line 97: | ||
]); |
]); |
||
}, |
}, |
||
− | /// Returns whether the user has any roles that could be self-removed, or a helper/staff/ |
+ | /// Returns whether the user has any roles that could be self-removed, or a helper/staff/soap |
hasNotableRoles: function() { |
hasNotableRoles: function() { |
||
return this.hasRights([ |
return this.hasRights([ |
||
Line 114: | Line 112: | ||
'helper', |
'helper', |
||
'staff', |
'staff', |
||
− | 'vstf', |
||
'soap' |
'soap' |
||
]); |
]); |
Revision as of 23:23, 4 August 2020
/* QuickLogs
*
* Displays user logs on Special:Contributions
*
* @author Slyst
*/
(function() {
// Scoping and double runs
if (
mw.config.get('wgCanonicalSpecialPageName') !== 'Contributions' ||
(window.QuickLogs && window.QuickLogs.loaded)
) {
return;
}
var QuickLogs = {
/// The name of the user whose contributions you're looking at
user: $('#UserProfileMasthead h1[itemprop="name"]').text(),
/// The wikia user ID of the user you're looking at
/// Only is defined when the current user is a chat moderator or above,
/// but that's fine because you only need it in those cases
userId: $('.chat-change-ban').data('user-id'),
/// Plain object holding mediawiki config values (and stylepath :P)
cfg: mw.config.get([
'wgMonthNames',
'wgUserGroups',
'wgUserName',
'stylepath'
]),
/// Reference to #quicklogs-ul
ul: null,
/// Double-run safety net
loaded: true,
/// Whether render was called or not
built: false,
/// Whether message walls are enabled in the current wiki
walls: false,
/// Whether Abuse Filter is enabled in the current wiki
abuseFilter: false,
/// Whether the current user can checkUser, this is defined at runtime by checking if there was a CU link before loading
checkUser: $('#contentSub [title="Special:CheckUser"]').exists(),
/// Whether chat exists on the wiki
chat: $('.chat-ban-log').exists(),
/// Internal variable for handling preloading
_loads: 0,
/// Called each time a resource is fetched
preload: function() {
if (++this._loads === 3) {
this.init();
}
},
/// Returns whether you're looking at your own contributions
ownPage: function() {
return this.user == this.cfg.wgUserName;
},
/// Returns whether you're not looking at your contribs and another function of passed
notOwnPageAnd: function(check) {
return !this.ownPage() && $.proxy(check, this)();
},
/// Returns whether the current user has any of the rights provided
hasRights: function(rights) {
var len = rights.length;
while (len--) {
if (this.cfg.wgUserGroups.indexOf(rights[len]) != -1) return true;
}
return false;
},
/// Returns whether the user has classic admin rights
isAdmin: function() {
return this.hasRights([
'bureaucrat',
'sysop',
'helper',
'staff',
'global-discussions-moderator',
'content-team-member',
'wiki-manager',
'soap'
]);
},
/// Returns whether the user can view deleted contributions
canViewDeletedRevs: function() {
return this.isAdmin() || this.hasRights(['content-moderator']);
},
/// Returns whether the user has classic chat mod rights
isChatMod: function() {
return this.chat && this.hasRights([
'chatmoderator',
'threadmoderator',
'sysop',
'helper',
'staff',
'content-team-member',
'wiki-manager',
'soap'
]);
},
/// Returns whether the user has any roles that could be self-removed, or a helper/staff/soap
hasNotableRoles: function() {
return this.hasRights([
'bureaucrat',
'sysop',
'content-moderator',
'threadmoderator',
'chatmoderator',
'rollback',
'bot',
'global-discussions-moderator',
'wiki-manager',
'vanguard',
'helper',
'staff',
'soap'
]);
},
/// Returns an anchor element with the supplied href and text, optionally striked-through
makeLink: function(href, text, striked, attr) {
return {
type: 'a',
classes: striked === false ? [] : ['link'],
attr: $.extend({
href: href
}, attr),
text: text
};
},
/// Returns an anchor element that calls this.load on click
makeLogLink: function(logType) {
var log = this.logTypes[logType];
return {
type: 'a',
classes: ['quicklogs'].concat(log.classes || []),
attr: {
'data-log': logType
},
events: {
click: this.load.bind(this, logType),
},
text: log.message || this.i18n.msg(log.key || logType + '-log').plain()
};
},
/// Toggles the selected class and calls the custom click handler or the default this.loadLogs
load: function(logType, e) {
if (e.target.classList && e.target.classList.contains('selected')) return;
var log = this.logTypes[logType];
if (log.click) {
return log.click(logType, log);
}
return this.loadLogs(logType, log);
},
/// Removes all currently selected quicklogs links and selects the one with the provided data-log attribute
setSelectedLog: function(logType) {
this.ul.className = 'displaying-' + logType;
$('.quicklogs.selected').removeClass('selected');
$('.quicklogs[data-log="' + logType + '"]').addClass('selected');
},
/// Clears the content of this.ul, begins a loading animation and sets the header text to loading
startLoading: function() {
this.ul.innerHTML = '';
this.header.textContent = this.i18n.msg('loading').plain();
dev.ui({
type: 'img',
style: {
display: 'block',
margin: '0 auto'
},
attr: {
src: this.cfg.stylepath + '/common/images/ajax.gif'
},
parent: this.ul
});
},
/// Loads classic log events by type
loadLogs: function(logType, log) {
this.startLoading();
this.setSelectedLog(logType);
var obj = {
action: 'query',
list: 'logevents',
leprop: 'title|user|timestamp|type|parsedcomment|details',
lelimit: 'max'
};
if (logType != 'logs') {
obj.letype = logType;
}
if (log.by) {
obj.leuser = log.by;
}
if (log.title) {
obj.letitle = 'User:' + log.title;
}
return this.api.get(obj)
.then(this.displayLogs.bind(this, logType, log));
},
/// Loads abuse log entries
loadAbuseLog: function(logType, log) {
this.startLoading();
this.setSelectedLog(logType);
this.api.get({
action: 'query',
list: 'abuselog',
afluser: this.user,
afllimit: 'max',
aflprop: 'ids|filter|user|title|action|result|timestamp|hidden'
})
.then(this.displayLogs.bind(this, logType, log));
},
/// Loads deletedrevs entries
loadDeletedContribs: function(logType, log) {
this.startLoading();
this.setSelectedLog(logType);
this.api.get({
action: 'query',
list: 'deletedrevs',
druser: this.user,
drlimit: 'max',
drprop: 'parsedcomment'
})
.then(this.displayLogs.bind(this, logType, log));
},
/// Displays any kind of log event
displayLogs: function(logType, log, data) {
var le = data.query && data.query.logevents || data.query.abuselog || data.query.deletedrevs,
children = [];
if (this.ul.className != 'displaying-' + logType) return;
if (data.error) {
children.push({
type: 'li',
text: this.i18n.msg('error', data.error.info, data.error.code).plain()
});
this.header.textContent = this.i18n.msg('error-header', logType).plain();
} else if ($.isEmptyObject(le)) {
children.push({
type: 'li',
text: this.i18n.msg('no-logs').plain()
});
this.header.textContent = this.i18n.msg('logs-header-selected', logType).plain();
} else {
for (var i in le) {
var html = this.format(le[i]);
if (html) {
children.push({
type: 'li',
html: this.format(le[i])
});
}
}
if (data['query-continue']) {
children.push({
type: 'li',
children: [
{
type: 'a',
attr: {
href: {
logs: mw.util.getUrl('Special:Log/' + this.user),
upload: mw.util.getUrl('Special:ListFiles/' + this.user),
abuse: mw.util.getUrl('Special:AbuseLog', {
wpSearchUser: this.user
})
}[logType] ||
mw.util.getUrl('Special:Logs/' + logType, {
user: log.by,
title: log.title
}),
},
text: this.i18n.msg('see-more').plain()
}
]
});
}
this.header.textContent = this.i18n.msg('logs-header-selected', logType).plain();
}
this.ul.innerHTML = '';
dev.ui({
children: children,
parent: this.ul
});
},
/// Formats a log event into an HTML string
format: function(ev) {
if (ev.move && ev.move.suppressedredirect) {
ev.action += '-without-redirect';
}
if (ev.action == 'move_redir') {
ev.action = 'move';
}
if (ev.action == 'patrol' && ev.patrol.auto) {
ev.action += '-auto';
}
if (ev.filter) {
ev.action = 'abuse';
}
if (!this.hasMessage('format-' + ev.action)) {
console.log('Missing format for ' + ev.action + '. Consider reporting this on w:dev:Talk:QuickLogs');
return '';
}
var date = this.date(ev.timestamp || ev.revisions[0].timestamp),
userLinks = this.userLinks(ev.user, true),
target = dev.ui({
type: 'a',
attr: {
href: mw.util.getUrl('User:' + this.user)
},
text: this.user
}).outerHTML,
page = dev.ui({
type: 'a',
attr: {
href: mw.util.getUrl(ev.title)
},
text: ev.title
}).outerHTML,
newTitle = !ev.move ? '' : dev.ui({
type: 'a',
attr: {
href: mw.util.getUrl(ev.move.new_title)
},
text: ev.move.new_title
}).outerHTML,
revision = !ev.patrol ? '' : dev.ui({
type: 'a',
attr: {
href: '/?diff=' + ev.patrol.cur
},
text: ev.patrol.cur.toString()
}).outerHTML,
comment = !ev.parsedcomment ? '' : dev.ui({
type: 'span',
classes: ['comment'],
html: '(' + ev.parsedcomment + ')'
}).outerHTML,
list = !ev.filter ? '' : dev.ui({
type: 'ul',
children: [
{
type: 'li',
text: this.i18n.msg('action', ev.action).plain()
},
{
type: 'li',
text: this.i18n.msg('actions-taken', ev.result).plain()
},
{
type: 'li',
text: this.i18n.msg('description', ev.filter).plain()
}
]
}).outerHTML,
rights = ev.rights ? this.parseRights(ev.rights) : {},
oldrights = rights.oldstr,
newrights = rights.newstr,
before = ev[1],
after = ev[0],
expiry = ev.action.indexOf('chatban') === 0 && ev[2] ? ev[2] : '',
ends = ev.action.indexOf('chatban') === 0 && ev[3] ? this.date(ev[3] * 1000) : '',
duration = ev.block && ev.block.duration ? ev.block.duration : '',
flags = ev.block && ev.block.flags ? ' (' + ev.block.flags.replace(/,/g, ', ') + ') ' : '';
if (ev.block || ev.type == 'chatban') {
target = page;
}
return this.parse('format-' + ev.action, {
date: date,
userLinks: userLinks,
target: target,
page: page,
newTitle: newTitle,
revision: revision,
comment: comment,
oldrights: oldrights,
newrights: newrights,
expiry: expiry,
duration: duration,
flags: flags,
list: list,
ends: ends,
before: before,
after: after
});
},
/// Parses named parameters that aren't currently supported in i18n-js
parse: function(msg, obj) {
return this.i18n.msg(msg).escape().replace(/\$([a-z]+)/gi, function(fullMatch, identifier) {
return obj[identifier] || '';
}).trim().replace(/(\s)+/g, function(_, first) {
return first;
});
},
/// Returns an object with old and new user rights strings, with changed groups highlighted
parseRights: function(rights) {
var oldarr = rights.old.split(', '),
newarr = rights['new'].split(', ');
return {
oldstr: oldarr.map(function(group) {
if (group && newarr.indexOf(group) == -1) return '<span class="quicklogs-group-diff">' + group + '</span>';
return group;
}).join(', ') || this.i18n.msg('none').escape(),
newstr: newarr.map(function(group) {
if (group && oldarr.indexOf(group) == -1) return '<span class="quicklogs-group-diff">' + group + '</span>';
return group;
}).join(', ') || this.i18n.msg('none').escape()
};
},
/// Returns whether the provided key is missing completely in translation. This does not include fallbacks to en.
hasMessage: function(key) {
return this.i18n.msg(key).plain() != '<' + key + '>';
},
/// Returns a date string defined by the specification in format-date
date: function(timestamp) {
var date = new Date(timestamp),
pad = function(n) {
return ('0' + n).slice(-2);
};
return this.parse('format-date', {
hours: pad(date.getUTCHours()),
minutes: pad(date.getUTCMinutes()),
seconds: pad(date.getUTCSeconds()),
date: date.getUTCDate(),
year: date.getUTCFullYear(),
month: this.cfg.wgMonthNames[date.getUTCMonth() + 1]
});
},
/// Generates a link to {user}'s userpage
/// If subpages is true then there will be additional talk, contribs, and block (if admin) links.
userLinks: function(user, subpages) {
nodes = [
{
type: 'a',
attr: {
href: mw.util.getUrl('User:' + user)
},
text: user
}
];
if (subpages) {
nodes.push(
' (',
this.walls
? {
type: 'a',
attr: {
href: mw.util.getUrl('Message Wall:' + user)
},
text: this.i18n.msg('wall').plain()
}
: {
type: 'a',
attr: {
href: mw.util.getUrl('User talk:' + user)
},
text: this.i18n.msg('talk').plain()
},
' | ',
{
type: 'a',
attr: {
href: mw.util.getUrl('Special:Contributions/' + user)
},
text: this.i18n.msg('contribs').plain()
}
);
if (this.isAdmin()) {
nodes.push(
' | ',
{
type: 'a',
attr: {
href: mw.util.getUrl('Special:Block/' + user)
},
text: this.i18n.msg('block').plain()
}
);
}
nodes.push(')');
}
return dev.ui({
type: 'div', // Document fragments don't implement innerHTML, much sad
children: nodes
}).innerHTML;
},
/// ext.Chat2.ChatBanModal but it's faster
/// FIXME: Try opening, closing, and opening it again
showBanModal: function() {
if (this.modal) {
this.modal.show();
}
},
preloadModal: function(uiFactory) {
$.when(
this.getModalData(),
uiFactory.init(['modal'])
).then($.proxy(this.createModal, this));
},
getModalData: function() {
return $.get(mw.util.wikiScript('index'), {
action: 'ajax',
rs: 'ChatAjax',
method: 'BanModal'
});
},
createModal: function(data, uiModal) {
this.modalHTML = data[0].template;
this.uiModal = uiModal;
this.modal = this.initModal();
},
initModal: function() {
this.uiModal.createComponent({
type: 'default',
vars: {
id: 'ChatBanModal',
size: 'small',
content: this.modalHTML,
title: mw.message('chat-ban-modal-change-ban-heading').plain(),
buttons: [
{
vars: {
value: mw.message('chat-ban-modal-button-ok').escaped(),
classes: ['normal', 'primary'],
data: [
{
key: 'event',
value: 'ok'
}
]
}
},
{
vars: {
value: mw.message('chat-ban-modal-button-cancel').escaped(),
data: [
{
key: 'event',
value: 'close'
}
]
}
}
]
}
}, $.proxy(this.cbInitModal, this));
},
cbInitModal: function(modal) {
this.modal = modal;
modal.$element
.find('input[name=reason]')
.placeholder()
.keydown($.proxy(this.cbKeydown, this));
modal.bind('ok', $.proxy(this.cbModal, this));
},
cbModal: function(event) {
event.preventDefault();
var $element = this.modal.$element;
$.post(mw.util.wikiScript('index'), {
action: 'ajax',
rs: 'ChatAjax',
method: 'blockOrBanChat',
userToBanId: this.userId,
time: $element.find('select[name=expires]').val(),
reason: $element.find('input[name=reason]').val(),
mode: 'global',
token: mw.user.tokens.get('editToken')
}, $.proxy(this.cbBan, this));
this.modal.trigger('close');
this.modal = null;
setTimeout($.proxy(this.initModal, this), 500);
},
cbBan: function() {
window.location.reload();
},
cbKeydown: function(event) {
if (event.which === 13) {
this.cbModal(event);
}
},
/// Renders the QuickLogs UI inside #contentSub
/// IMPORTANT: This function has the potential of being called multiple times, in case new quick links are registered,
/// or called directly by another script to refresh the UI.
render: function() {
var i = 0,
children = [
this.i18n.msg('for').plain() + ' ',
this.makeLink(mw.util.getUrl('User:' + this.user), this.user),
' ('
],
contentSub = document.getElementById('contentSub'),
logType,
log;
if (!contentSub) return; // wot
while (logType = this.logOrder[i++]) {
log = this.logTypes[logType];
if (!log.check || log.check()) {
if (log.href) {
children.push(this.makeLink(log.href, log.message || this.i18n.msg(log.key).plain(), log.striked, log.attr));
} else {
children.push(this.makeLogLink(logType));
}
children.push(' | ');
}
}
contentSub.innerHTML = '';
contentSub.className += ' quicklogs__content-sub';
children[children.length - 1] = ')';
dev.ui({
children: children,
parent: contentSub
});
if (!this.built) {
this.ul = dev.ui({
type: 'ul',
children: [
{
type: 'li',
text: this.i18n.msg('select-logs').plain()
}
]
});
this.header = dev.ui({
type: 'div',
attr: {
id: 'quicklogs-head'
},
text: this.i18n.msg('logs-header').plain()
});
contentSub.parentElement.insertBefore(
dev.ui({
type: 'div',
attr: {
// This is dumb, this class is already used in contentSub, but I'm keeping it for legacy reasons
// Just make sure you use the ID to get this element
'class': 'quicklogs__content-sub',
id: 'quicklogs-container'
},
children: [
this.header,
{
type: 'div',
attr: {
// SYKE IT WASNT AN UL, IT WAS JUST A CONTAINER
id: 'quicklogs-ul'
},
children: [this.ul]
}
]
}),
contentSub.nextElementSibling
);
}
var built = this.built;
this.built = true;
mw.hook('QuickLogs.render').fire(QuickLogs);
if (!built) {
mw.hook('QuickLogs.render.first').fire(QuickLogs);
}
},
/// For usage in other scripts, registers a new link for the QuickLogs toolbar and re-renders in necessary
addLink: function(key, obj, index) {
if (this.logTypes[key]) return false;
this.logTypes[key] = obj;
this.logOrder.splice(index === undefined ? this.logOrder.length : index, 0, key);
if (this.built) {
this.render();
}
return true;
},
/// For usage in other scripts, removes a registered link for the toolbar and re-renders
removeLink: function(keyOrIndex, render) {
var key,
removed,
index;
if (typeof keyOrIndex === 'number') {
key = this.logOrder.splice(keyOrIndex, 1)[0];
removed = delete this.logTypes[key];
} else {
key = keyOrIndex;
removed = delete this.logTypes[key];
index = this.logOrder.indexOf(key);
if (index != -1) {
this.logOrder.splice(index, 1);
}
}
if (removed && this.built && render !== false) {
this.render();
}
return removed;
},
// For usage in other scripts, removes multiple links
removeLinks: function() {
var i = arguments.length,
removed;
while (i--) {
removed = removed || this.removeLink(arguments[i], false);
}
if (removed && this.built) {
this.render();
}
return removed;
},
/// Function to be called after all necessary resources have been fetched
init: function() {
this.api = new mw.Api();
this.render();
},
/// Check if AbuseFilter is enabled
checkAF: function() {
var promise = $.Deferred();
$.get(mw.util.wikiScript('api'), {
action: 'query',
meta: 'siteinfo',
siprop: 'extensions',
format: 'json'
}).done(function(d) {
promise.resolve(d.query.extensions.filter(function(ext) {
return ext.name == 'Abuse Filter';
}).length > 0);
});
return promise;
},
/// Fork from WgMessageWallsExist because such script does not provide suitable bindings
checkWall: function() {
var promise = $.Deferred(),
stupidWikis = ['177'];
if (stupidWikis.indexOf(wgCityId) !== -1) return promise.resolve(true);
$.nirvana.getJson('WikiFeaturesSpecialController', 'index').done(function(d) {
var disabled =
d.features.filter(function (t) {
return t.name === 'wgEnableWallExt' && t.enabled;
}).length === 0;
if (disabled) {
promise.resolve(false);
} else {
promise.resolve(true);
}
}).error(function() {
var wall = '.wds-global-navigation__dropdown-link[data-tracking-label="account.message-wall"]';
if (document.querySelector(wall) === null) {
promise.resolve(false);
} else {
promise.resolve(true);
}
});
return promise;
},
i18n: function(i18n) {
i18n.loadMessages('QuickLogs').then($.proxy(QuickLogs.cbI18n, QuickLogs));
},
cbI18n: function(i18nd) {
this.i18n = i18nd;
this.preload();
},
cbData: function(af, wall) {
this.abuseFilter = af;
this.walls = wall;
if (this.logTypes.wall) {
this.logTypes.wall.key = wall ? 'wall' : 'talk';
this.logTypes.wall.href = mw.util.getUrl((wall ? 'Message Wall:' : 'User talk:') + this.user);
}
this.preload();
}
};
QuickLogs.logTypes = $.extend({
wall: {
href: mw.util.getUrl('User talk:' + QuickLogs.user),
key: 'talk'
},
blockLink: {
check: QuickLogs.notOwnPageAnd.bind(QuickLogs, QuickLogs.isAdmin),
href: mw.util.getUrl('Special:Block/' + QuickLogs.user),
key: 'block'
},
block: {
title: QuickLogs.user
},
upload: {
by: QuickLogs.user
},
move: {
by: QuickLogs.user
},
logs: {
by: QuickLogs.user,
key: 'logs'
},
checkUserLink: {
check: function() { return !QuickLogs.ownPage() && QuickLogs.checkUser },
href: mw.util.getUrl('Special:CheckUser', {
user: QuickLogs.user
}),
key: 'check-user-ip'
},
abuse: {
check: function() { return QuickLogs.abuseFilter },
click: QuickLogs.loadAbuseLog.bind(QuickLogs)
},
rights: {
title: QuickLogs.user
},
deletedcontribs: {
check: QuickLogs.canViewDeletedRevs.bind(QuickLogs),
click: QuickLogs.loadDeletedContribs.bind(QuickLogs)
},
rightsLink: {
check: function() {
return QuickLogs.isAdmin() ||
QuickLogs.ownPage() && QuickLogs.hasNotableRoles();
},
href: mw.util.getUrl('Special:UserRights/' + QuickLogs.user),
key: 'rights-management'
},
chatban: {
check: function() {
return QuickLogs.chat;
},
title: QuickLogs.user
},
chatbanLink: {
check: QuickLogs.notOwnPageAnd.bind(QuickLogs, QuickLogs.isChatMod),
click: QuickLogs.showBanModal.bind(QuickLogs),
striked: false,
key: 'ban-from-chat'
}
}, (window.QuickLogs || {}).logTypes);
QuickLogs.logOrder = ['wall', 'blockLink', 'block', 'upload', 'move', 'logs', 'checkUserLink', 'abuse', 'rights', 'deletedcontribs', 'rightsLink', 'chatban', 'chatbanLink'];
// Expose resources
window.QuickLogs = QuickLogs;
mw.hook('QuickLogs.loaded').fire(QuickLogs);
// Loading necessary resources
importArticles(
{
type: 'script',
articles: [
'u:dev:MediaWiki:I18n-js/code.js',
'u:dev:MediaWiki:UI-js/code.js'
]
},
{
type: 'style',
articles: [
'u:dev:MediaWiki:QuickLogs.css'
]
}
);
mw.hook('dev.ui').add(QuickLogs.preload.bind(QuickLogs));
mw.hook('dev.i18n').add(QuickLogs.i18n.bind(QuickLogs));
$.when(
QuickLogs.checkAF(),
QuickLogs.checkWall(),
mw.loader.using([
'mediawiki.api',
'mediawiki.user',
'mediawiki.util'
])
).then($.proxy(QuickLogs.cbData, QuickLogs));
require(['wikia.ui.factory'], $.proxy(QuickLogs.preloadModal, QuickLogs));
})();