Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
- Opera: Press Ctrl-F5.
// TopicBlockLog (v1.2.1)
// @author: The JoTS
// Creates an interwiki block report from wikis of similar topic.
// This allows an administrator to more easily identify editors who may be
// making edits with ill intent or identify topic "raiders".
// todo: TBL_PATROL var (see doc page)
// Run only on [[Special:Block]] w/ valid user permissions & not post-blocking.
if (mw.config.get('wgCanonicalSpecialPageName') === 'Block'
&& $("#mw-content-text form").length)
// Script body
(function($, mw, window) {
"use strict";
if (window.TBL_DEBOUNCE || !(window.TBL_DEBOUNCE=true)) return;
///************/
//* "Consts" *//
/************///
var DELAY = 40, // 25 requests per sec
REQS_BEFORE_PAUSE = 60, // tbh, I don't know how much rps is too much.
PAUSE_DELAY = 80,
DEBUG_MSGS = {
// These messages are (mostly) not user facing.
// Debug warnings (not all are stored here)
HAD_LOCAL_BLOCK: "Target user had been blocked on local wiki. Pre-existing logs available.",
NO_LOCAL_BLOCK: "Target user has never been blocked on local wiki. Create log.",
NOT_BLOCK_PAGE:
"Page is not [[Special:Block]] or user does not have permission to view it.",
// Script errors
STATIC_USED_IN_INSTANCE:
"Attempt to call a class static function on a class instance",
// User configuration errors
EXEC_ERR:
"An error occurred before ajax requests were sent.\n" +
"Did you set TBL_GROUP or TBL_WIKIS? If used, is TBL_GROUP a valid topic?"
};
///********/
//* Data *//
/********///
var
api,
config = mw.config.get([
"wgPageName",
"wgUserLanguage",
"wgServer"
]),
requestMessages = [
// general
"pipe-separator",
"word-separator",
// user
"messagewall-contributiontools-label",
"contribslink",
// block
"blocklog-showlog",
"blocklogentry",
"unblocklogentry"
];
config = $.extend(config, {
usingLang: mw.util.getParamValue("uselang")
|| config.wgUserLanguage.split('-')[0],
targetUser: config.wgPageName.split("/")[1],
wiki: /(?=(?:[a-z]+\.)?(\w+)\.(?:wikia|fandom)\.com)/
.exec(config.wgServer)[1]
});
// No target (no /Username nor wpTarget=Username)
if (!(config.targetUser = config.targetUser
|| mw.util.getParamValue("wpTarget"))) return;
///*******************/
//* Element Factory *//
/*******************///
function Make(tag, classes) {
// I found out too late that I cannot use the class declaration for JS on wikia
// So... here's a class of different syntax with fake static methods.
this.e = document.createElement(tag); // even if a class declaration could be used, cannot extend the class from DOM elements, yet.
if (typeof classes !== "undefined")
this.setClass(classes);
}
Make.prototype.isObject = true; // for static function checking
// Static class constructors
// todo: apparently __proto__ is legacy, use the getter method instead.
Make.prototype.link = function(index, page, ltext, DANGER_TRUST) {
// todo? DANGER_TRUST will assume sanitized input is provided.
if (this.__proto__.isObject) throw STATIC_USED_IN_INSTANCE;
var a = new Make('a');
a.e.setAttribute("href", encodeURI(index+page));
// a.e.setAttribute("title", escape(page));
if (ltext) a.e.innerHTML = ltext;
else a.e.innerHTML = page;
return a;
};
Make.prototype.toolLinks = function(mlinks) {
if (this.__proto__.isObject) throw STATIC_USED_IN_INSTANCE;
var tools = new Make('span', 'mw-usertoollinks')
.contains('(');
for (var i = 0; i < mlinks.length;)
tools.append(mlinks[i] +
(++i !== mlinks.length ? (' '+mw.message("pipe-separator")+' ') : ')'));
return tools;
};
Make.prototype.comment = function(wiki, _comment) {
if (this.__proto__.isObject) throw STATIC_USED_IN_INSTANCE;
return new Make('i')
.contains('('
+ _comment.replace(/\[\[(.+?)(\|(.+?))?\]\]/g, function(full, wlink, _, wtext) {
return Make.prototype.link(wiki, wlink, wtext)})
+ ')');
};
// Object methods
// Should only be used with sanitized html content
Make.prototype.setClass = function(classes) {
this.e.setAttribute("class", classes);
return this;
};
Make.prototype.contains = function(content) {
this.e.innerHTML = content;
return this;
};
Make.prototype.append = function(content) {
this.e.innerHTML += content;
return this;
};
// @override
Make.prototype.toString = function() { return this.e.outerHTML; };
///****************************/
//* Report Container & Flags *//
/****************************///
var $blockLog = $(".mw-warning-with-logexcerpt"), $loading,
blockedNotice,
hadAnyBlock,
nWikisParsed = 0, // # of wikis finished parsing
nFinalParsed = 0; // # of wikis to be parsed at finish
// currParseIntv;
if($blockLog.length) {
// Target user had been blocked on local wiki. Pre-existing logs available.
mw.log(DEBUG_MSGS.HAD_LOCAL_BLOCK);
$blockLog.append(document.createElement("hr"));
hadAnyBlock = true;
} else {
// Target user has never been blocked on local wiki. Create log.
mw.log(DEBUG_MSGS.NO_LOCAL_BLOCK);
$("#mw-content-text").append(
$blockLog = $(document.createElement("div"))
.addClass("mw-warning-with-logexcerpt warningbox mw-content-ltr")
.append(blockedNotice = document.createElement("p"))
);
hadAnyBlock = false;
}
$loading = $(
// http://fandomdesignsystem.com/#/components/progress-indicators
'<svg xmlns="http://www.w3.org/2000/svg" class="wds-spinner wds-spinner__block" width="45" height="45" viewBox="0 0 45 45"> \
<g transform="translate(22.5, 22.5)"> \
<circle class="wds-spinner__stroke" fill="none" stroke-linecap="round" stroke-width="5" stroke-dasharray="125.66370614359172" stroke-dashoffset="125.66370614359172" r="20"></circle> \
</g> \
</svg>')
.appendTo("#mw-content-text");
$blockLog.hide(); // while loading
///*****************/
//* Log Retrieval *//
/*****************///
function getInterwikiLog(wiki, callback) {
// e.g. http://campcamp.wikia.com/api.php?action=query&list=logevents&letype=block&letitle=User:The_JoTS&format=json
if ( (wiki=wiki.split('.'))[wiki.length - 1] === config.wiki ) {
// No need to request and re-render what is already rendered.
recordRenderedWiki();
return;
}
mw.log("Checking user's block log at wiki " + wiki);
$.get("https://" + wiki + ".fandom.com/api.php", { // does not support wikia.org at this time, todo
action: "query",
format: "json",
// Block logs
list: "logevents",
letype: "block",
letitle: "User:" + config.targetUser,
// SiteInfo
meta: "siteinfo",
siprop: "general"
}, function(result) {
if (result !== null
&& result.query.logevents.length !== 0)
callback(result.query, recordRenderedWiki);
else recordRenderedWiki();
if (result === null)
// I'm not sure if this result is a possible case?
mw.log("Null result returned from query. Sending in queries too quickly?");
}, "jsonp")
.fail(function() {
mw.log("Failed to retrieve block logs at wiki " + wiki);
recordRenderedWiki();
});
}
///********************/
//* Make log entries *//
/********************///
function makeWikiEntries(result, callback) {
var $localLogs,
indexURL = result.general.server + "/index.php?title=";
mw.log("Creating block entries for current user from " + result.general.server);
// New wiki section
$blockLog.append($(document.createElement("div"))
.prepend($localLogs = $(document.createElement("ul")))
.prepend(new Make("h4")
.contains( Make.prototype.link(result.general.server, '', result.general.sitename) ).e
)
);
// Log entry
result.logevents.reverse().forEach(function(entry) {
hadAnyBlock = true;
var liEntry = new Make("li"),
blocked = entry.action !== "unblock", // there's a "change block" or something apparently
blockedOn = new Date(entry.timestamp);
blockedOn = $.extend(blockedOn, {
// this would've been a *great* time for toLocaleFormat() to be supported...
hour: String(blockedOn.getHours()).padStart(2,'0'),
minute: String(blockedOn.getMinutes()).padStart(2,'0'),
date: blockedOn.getDate(),
month: mw.config.get("wgMonthNames")[blockedOn.getMonth() + 1], // todo: respect uselang url param
year: blockedOn.getFullYear()
});
//
var target = entry.title.replace(/^.+?:/,''),
targetUserLinks = Make.prototype.link(indexURL, entry.title, target) + ' '
+ Make.prototype.toolLinks([
Make.prototype.link(indexURL, "User talk:"+target, mw.message("messagewall-contributiontools-label")),
Make.prototype.link(indexURL, "Special:Contributions/"+target, mw.message("contribslink"))
]);
liEntry.contains(
// Note: mw.message(loadedmsg).parse() didn't seem to work on my end and thus was not used in this script.
// Because of this, some languages' messages may have some formatting errors.
blockedOn.hour + ':' + blockedOn.minute + ', '
+ blockedOn.month + ' ' + blockedOn.date + ", " + blockedOn.year + ' '
+ Make.prototype.link(indexURL, "User:"+entry.user, entry.user) + ' '
+ Make.prototype.toolLinks([
Make.prototype.link(indexURL, "User talk:"+entry.user, mw.message("messagewall-contributiontools-label")),
Make.prototype.link(indexURL, "Special:Contributions/"+entry.user, mw.message("contribslink"))
]) + ' ' + (blocked
? mw.message("blocklogentry", targetUserLinks, entry.params.duration, '')
.plain().replace(/\[|\]/g,'')
: mw.message("unblocklogentry", targetUserLinks)
) + ' ' + Make.prototype.comment(indexURL, entry.comment)
);
$(liEntry.e)
.addClass("mw-logline-block")
.prependTo($localLogs);
});
callback();
}
///***************/
//* Async Tasks *//
/***************///
function recordRenderedWiki() {
if (++nWikisParsed !== nFinalParsed)
// Incremented, now be on our merry way~
mw.log("A parse task (" + nWikisParsed + "/" + nFinalParsed + ") finished.");
else {
// Finished rendering!
mw.log("A parse task (Last/" + nFinalParsed + ") finished.");
// Plug script on Dev Wiki
$("<div style='text-align:right; font-size:80%;'>"
+ Make.prototype.link("http://dev.fandom.com/wiki/TopicBlockLog", '',
"Interwiki block report by TopicBlockLog")
+ "</div>")
.appendTo($blockLog);
// Show log and stop load animation
if (hadAnyBlock) $blockLog.show();
else $blockLog.remove();
$loading.remove();
mw.log("TopicBlockLog render tasks completed.");
mw.log(hadAnyBlock
? "Target user was blocked locally and/or blocked on sister wikis."
: "Target user does not have any block history on wikis of this topic.");
}
}
function getAndRenderLogs(wikiIter, finalParseCount) {
nFinalParsed = finalParseCount;
getAndRenderLogF(wikiIter)();
}
function getAndRenderLogF(wikiIter) {
return function() {
var reqSent = 0,
requestIntv = setInterval(function() {
var wiki = wikiIter.next();
if (!wiki.done) {
// Send out another log request.
getInterwikiLog(wiki.value, makeWikiEntries);
if (++reqSent % REQS_BEFORE_PAUSE === 0) {
mw.log("Pausing TopicBlockLog ajax requests for " + REQS_BEFORE_PAUSE + "ms.");
setTimeout(getAndRenderLogF(wikiIter), PAUSE_DELAY);
clearInterval(requestIntv);
}
} else {
// Last ajax request sent. Terminate interval.
mw.log("Last ajax query sent for TopicBlockLog.");
clearInterval(requestIntv);
return;
}
}, DELAY);
};
}
function init() {
api.get({
action: 'parse',
prop: 'wikitext',
formatversion: '2',
page: 'MediaWiki:Custom-TopicBlockLog-topics.json'
}).then(function(data) {
if (data.parse) {
var TOPICS = JSON.parse(data.parse.wikitext.replace(/\/\*(.|\s)*?\*\//g, '')),
userDefWikiList = window.TBL_WIKIS,
userSelWikiTopic = window.TBL_GROUP;
if (typeof userDefWikiList !== "undefined"
&& Array.isArray(userDefWikiList))
// Get logs from user defined list of wikis
getAndRenderLogs(
userDefWikiList[Symbol.iterator](),
userDefWikiList.length );
else if (typeof userSelWikiTopic === "string"
&& typeof TOPICS !== "undefined"
&& TOPICS[userSelWikiTopic.toLowerCase()])
// Get logs from wikis of a predefined topic group
getAndRenderLogs(
TOPICS[userSelWikiTopic][Symbol.iterator](),
TOPICS[userSelWikiTopic].length );
// todo: else automatically search for wiki w/in "TOPICS", maybe?
else {
// Something's gone wrong. Likely no setup vars were provided by user.
// ... or invalid topic group.
$loading.remove();
$blockLog.show();
console.warn(DEBUG_MSGS.EXEC_ERR);
}
}
});
}
///*********/
//* Start *//
/*********///
mw.loader.using('mediawiki.api').then(function() {
api = new mw.Api({
ajax: {
url: 'https://dev.fandom.com/api.php',
xhrFields: {
withCredentials: true
},
dataType: 'JSONP',
crossDomain: true
}
});
function loadMessages( messages ) {
return (new mw.Api()).get( {
action: 'query',
meta: 'allmessages',
ammessages: messages.join('|'),
amlang: config.usingLang
} ).then( function (data) {
$.each( data.query.allmessages, function ( i, message ) {
if ( message.missing !== '' ) {
mw.messages.set( message.name, message['*'] );
}
} );
} );
}
loadMessages(requestMessages).then(function() {
$(blockedNotice).text(mw.message("blocklog-showlog").text());
});
init();
});
})(jQuery, mediaWiki, this);