User:Rillke/globalMessageDelivery.js
Jump to navigation
Jump to search
Note: After saving, you have to bypass your browser's cache to see the changes. Internet Explorer: press Ctrl-F5, Mozilla: hold down Shift while clicking Reload (or press Ctrl-Shift-R), Opera/Konqueror: press F5, Safari: hold down Shift + Alt while clicking Reload, Chrome: hold down Shift while clicking Reload.
Documentation for this user script can be added at User:Rillke/globalMessageDelivery. |
- Report page listing warnings and errors.
/**
* WARNING: This script was never finished.
* See https://meta.wikimedia.org/wiki/MassMessage instead.
*
* GlobalMessage delivery
* JavaScript Application
*
* A dialog containing options, capable sending
* localized messages within the Wikimedia Empire
* to predifined addressees
*
*
* @rev 1 (2013-01-19)
* @author [[:commons:User:Rillke]], 2013
* @license GPL v.3
*/
// List the global variables for jsHint-Validation. Please make sure that it passes http://jshint.com/
// Scheme: globalVariable:allowOverwriting[, globalVariable:allowOverwriting][, globalVariable:allowOverwriting]
/*global jQuery:false, mediaWiki:false*/
// Set jsHint-options. You should not set forin or undef to false if your script does not validate.
/*jshint forin:true, noarg:true, noempty:true, eqeqeq:true, bitwise:true, strict:true, undef:true, curly:false, browser:true*/
( function ( $, mw ) {
"use strict";
// Style defs
// TODO: Credits for icon authors
mw.util.addCSS(
'li.gmd-doing { padding-left:16px; background:url(\'' +
'//upload.wikimedia.org/wikipedia/commons/thumb/4/41/Crystal_Clear_action_loopnone.png/16px-Crystal_Clear_action_loopnone.png' +
'\') no-repeat left; font-weight:bold; } ' +
'li.gmd-done { padding-left:16px; background:url(\'' +
'//upload.wikimedia.org/wikipedia/commons/thumb/a/ac/Crystal_Project_success.png/16px-Crystal_Project_success.png' +
'\') no-repeat left; } ' +
'li.gmd-failed { padding-left:16px; background-color: #F2CBCB; background:url(\'' +
'//upload.wikimedia.org/wikipedia/commons/thumb/c/c8/Crystal_Project_cancel.png/16px-Crystal_Project_cancel.png' +
'\') no-repeat left; } ' +
'li.gmd-unknown { padding-left:16px; background-color: #F2EECB; background:url(\'' +
'//upload.wikimedia.org/wikipedia/commons/thumb/a/af/Crystal_Clear_app_error-info.png/16px-Crystal_Clear_app_error-info.png' +
'\') no-repeat left; } ' +
'li.gmd-ratelimited { padding-left:16px; background:url(\'' +
'//upload.wikimedia.org/wikipedia/commons/thumb/c/c7/Time_Trial.svg/16px-Time_Trial.svg.png' +
'\') no-repeat left; } '
);
// A bunch of helper functions
function isNumber(n) {
return !isNaN(parseInt(n, 10)) && isFinite(n);
}
function firstItem(o) { for (var i in o) { if (o.hasOwnProperty(i)) { return o[i]; } } }
function firstItemName(o) { for (var i in o) { if (o.hasOwnProperty(i)) { return i; } } }
var CORSsupported = false;
var testCORS = function(done) {
if (CORSsupported) return done();
doCORSReq({
action: 'query',
meta: 'userinfo'
}, 'www.mediawiki.org', function(data, textStatus, jqXHR) {
if (!data.query.userinfo.id) {
CORSsupported = 'CORS supported but you are not globally logged-in. Please accept Cookies from third parties while signing-in.';
} else {
CORSsupported = 'OK';
}
done(CORSsupported);
}, function(jqXHR, textStatus, errorThrown) {
CORSsupported = 'CORS not supported: ' + textStatus + ' \nError: ' + errorThrown;
done(CORSsupported);
});
};
var doCORSReq = function(params, wiki, cb, errCb, method) {
$.support.cors = true;
// Format parameter first!
var newParams = {
format: 'json',
origin: document.location.protocol + '//' + document.location.hostname
};
params = $.extend(newParams, params);
$.ajax({
'url': '//' + wiki + '/w/api.php',
'data': params,
'xhrFields': {
'withCredentials': true
},
'type': method || 'GET',
'success': function(r) {
cb(r, wiki);
},
'error': errCb,
'dataType': 'json'
});
};
var gmd;
gmd = {
install: function() {
var $p = mw.util.addPortletLink('p-tb', '#globalMessageDelivery', 'Gl. Message Delivery', 't-gmd');
if (!$p) return gmd.log("Can't install. mw.util.addPortletLink returned ", $p);
$p = $($p);
$p.click($.proxy(gmd.launch, gmd));
},
$dlg: $('<div>').attr({
title: "Global Message Delivery - Distribute localized messages to a list of receivers"
}),
config: {
dlg: {
width: 850,
height: ($(window).height() > 770 ? 'auto' : $(window).height())
},
distroPrefix: 'Rillke/gmd/',
distroNs: 2,
localapi: mw.util.wikiScript('api')
},
launch: function(e) {
// prevent jumping to top
if (e) e.preventDefault();
this.queryAPI({
action: 'sitematrix'
}, 'gotSitematrix');
},
gotSitematrix: function(r) {
gmd.sitematrix = {};
gmd.knownLangCodes = {};
$.each(r.sitematrix, function (i, sites) {
if (!isNumber(i)) return;
$.each(sites.site, function (x, s) {
var domain = s.url.replace(/^https?\:\/\//, '');
gmd.sitematrix[domain] = {
api: s.url.replace('http://', '//') + '/w/api.php',
dbname: s.dbname,
domain: domain,
langcode: sites.code,
typecode: s.code
};
});
gmd.knownLangCodes[sites.code] = true;
});
$.each(r.sitematrix.specials, function (i, s) {
var domain = s.url.replace(/^https?\:\/\//, '');
gmd.sitematrix[domain] = {
api: s.url.replace('http://', document.location.protocol + '//') + '/w/api.php',
dbname: s.dbname,
domain: domain,
specialcode: s.code
};
});
gmd.secureCall("dialog");
},
dialog: function() {
var _this = gmd,
dlgButtons = {},
$submitButton,
running = false,
abort = false;
var $CORSStatus = $('<span>').text("CORS is required for this tool. The CORS status of you system: ").appendTo(this.$dlg),
$recips = $('<div>').appendTo(this.$dlg),
$recipsH = $('<h2>').text("1) Select recipient list").appendTo($recips),
$recipsSel = $('<select>').appendTo($recips),
$msg = $('<div>').appendTo(this.$dlg),
$msgH = $('<h2>').text("2) Enter message location (e.g. User:Rillke/messages)").appendTo($msg),
$msgLoc = $('<input size="120">').attr({
'placeholder': "Page containing message which has subpages whose names are the langcodes"
}).appendTo($msg),
$settings = $('<div>').appendTo(this.$dlg),
$settingsH = $('<h2>').text("3) Adjust whom to notify").appendTo($settings),
$settingsMissingI18nWrap = $('<div>').appendTo($settings),
$settingsMissingI18nL = $('<label>').text("If no translation is available, ").attr({
'for': 'gmdSettMissingI18n'
}).appendTo($settingsMissingI18nWrap),
$settingsMissingI18n = $('<select id="gmdSettMissingI18n">').appendTo($settingsMissingI18nWrap),
$settingsDefaultLWrap = $('<div>').appendTo($settings),
$settingsDefaultLL = $('<label>').text("Default language: ").attr({
'for': 'gmdSettDefL'
}).appendTo($settingsDefaultLWrap),
$settingsDefaultL = $('<select id="gmdSettDefL" disabled="disabled">').appendTo($settingsDefaultLWrap),
$msgOptions = $('<div>').appendTo(this.$dlg),
$msgOptionsH = $('<h2>').text("4) Message options").prependTo($msgOptions),
$msgOptionsEditSummaryWrap = $('<div>').appendTo($msgOptions),
$msgOptionsEditSummaryL = $('<label>').text("Edit summary: ").attr({
'for': 'gmdOptEditSummary'
}).appendTo($msgOptionsEditSummaryWrap),
$msgOptionsEditSummary = $('<input size="120" maxlength="240" id="gmdOptEditSummary">').attr({
'placeholder': "Append to edit summary (h2 headings are automatically detected)"
}).appendTo($msgOptionsEditSummaryWrap),
$msgOptionsPrependWrap = $('<div>').appendTo($msgOptions),
$msgOptionsPrependL = $('<label>').text("Prepend text (e.g. a heading)").attr({
'for': 'gmdOptTxtPrepend'
}).appendTo($msgOptionsPrependWrap),
$msgOptionsPrepend = $('<textarea>').css({
height: '40px',
width: '99%'
}).attr({
'placeholder': "Prepend text",
'id': 'gmdOptTxtPrepend'
}).appendTo($msgOptionsPrependWrap),
$msgOptionsAppendWrap = $('<div>').appendTo($msgOptions),
$msgOptionsAppendL = $('<label>').text("Append text (e.g. ''[[User:Rillke|Rillke]]''<sup>[[User talk:Rillke|(q?)]]</sup> 17:42, 26 January 2013 (UTC))").attr({
'for': 'gmdOptTxtAppend'
}).appendTo($msgOptionsAppendWrap),
$msgOptionsAppend = $('<textarea>').css({
height: '40px',
width: '99%',
'id': 'gmdOptTxtAppend'
}).attr({
'placeholder': "Append text"
}).appendTo($msgOptionsAppendWrap),
$confirmSection = $('<div>').css('min-height', '300px').appendTo(this.$dlg),
$confirmSectionH = $('<h2>').text("5) Get a preview").appendTo($confirmSection),
$confirmSectionWait = $('<div>').css({ 'max-height': 280, 'overflow': 'auto', 'text-align': 'center', 'font-size': '2em', 'margin-top': '100px' }).text("No preview available. Fill-in all fields, please.").appendTo($confirmSection),
$confirmSectionList = $('<ul>').css({ 'max-height': 280, 'overflow': 'auto', background: 'white' });
// Fill selects
$('<option>').attr('value', 'defLang').text("use the default language").appendTo($settingsMissingI18n);
$('<option>').attr('value', 'dontDistribute').text("do not distribute the message to this location").appendTo($settingsMissingI18n);
var msgXHR,
lastVal;
var _msgLocationChange = function() {
var pgn = $msgLoc.val(), // page name
pgnc = pgn.replace(/\/$/, '') + '/',
nsi = gmd.namspaceInfo(pgnc);
gmd.currentI18nStore = nsi;
$confirmSectionList.children().remove();
$confirmSectionWait.appendTo($confirmSection);
$confirmSectionWait.text("Fetching message languages...");
_abortIfRunning();
msgXHR = gmd.queryAPI({
action: 'query',
generator: 'allpages',
gapprefix: nsi.title,
gapnamespace: nsi.ns,
gapfilterredir: 'nonredirects',
gaplimit: 100,
prop: 'revisions',
rvprop: 'content'
}, $.proxy(gmd._gotTranslations, gmd, $settingsDefaultL, $confirmSectionWait, _updateUI), undefined, 'GET', true);
};
var _abortIfRunning = function() {
if (running) {
gmd.log("Aborting parsing distro list.");
abort = true;
}
};
var _updateUI = function() {
_abortIfRunning();
_parseDistroList(function() {
gmd.log('_parseDistroList');
$confirmSectionList.detach().children().remove();
$confirmSectionWait.appendTo($confirmSection);
$confirmSectionWait.text("Compiling message list...");
if (abort) {
abort = false;
return;
}
running = true;
var $chks = $(),
i = 0,
l = gmd.distroList.length;
// FIXME: whileAsync is unsupported
$.whileAsync({
delay: 2,
bulk: 100,
test: function () {
return (i < l && !abort);
},
loop: function () {
var di = gmd.distroList[i],
id = 'distroItem' + i;
gmd.log('_addingCheckbox');
di.$chk = $('<input type="checkbox" checked="checked" id="' + id + '"/>');
$chks = $chks.add(di.$chk);
di.$li = $('<li>').append(di.$chk, $('<label>').text(di.lang + ' to ').attr({ 'for': id })
.append(di.tolink.attr('target', '_blank'))).appendTo($confirmSectionList);
i++;
},
end: function () {
if (abort) {
$confirmSectionWait.text("Aborted...");
} else {
$confirmSectionWait.detach();
$confirmSectionList.appendTo($confirmSection);
$chks.checkboxShiftClick();
}
abort = false;
running = false;
}
});
});
};
$settingsDefaultL.change(_updateUI);
$settingsMissingI18n.change(_updateUI);
$recipsSel.change(_updateUI);
$settingsMissingI18n.change(function() {
if ($(this).val() === 'dontDistribute') {
$settingsDefaultL.attr('disabled', 'disabled');
} else {
$settingsDefaultL.removeAttr('disabled');
}
});
var to = 0;
$msgLoc.on('change keyup input', function() {
// Check whether there was indeed a change
var newVal = $(this).val();
if (newVal === lastVal) return;
lastVal = newVal;
$settingsDefaultL.attr('disabled', 'disabled');
if (msgXHR) msgXHR.abort();
gmd.i18nStore = {};
clearTimeout(to);
to = setTimeout(_msgLocationChange, 1000);
});
this.queryAPI({
action: 'query',
generator: 'allpages',
gapprefix: this.config.distroPrefix,
gapnamespace: this.config.distroNs,
gapfilterredir: 'nonredirects',
gaplimit: 100,
prop: 'revisions',
rvprop: 'content'
}, $.proxy(this._gotDistributionList, this, $recipsSel), this.sitematrix['commons.wikimedia.org'].api, 'GET', true);
dlgButtons["Start"] = function() {
_send();
};
dlgButtons["Cancel"] = function() {
$(this).dialog('close');
};
this.$dlg.dialog({
modal: true,
closeOnEscape: true,
position: 'center',
height: this.config.dlg.height,
width: Math.min($(window).width(), this.config.dlg.width),
buttons: dlgButtons,
close: function () {
$(this).dialog('destroy');
$(this).remove();
$('.tipsy').remove();
_this.dlgPresent = false;
},
open: function () {
var $dlg = $(this);
$dlg.parents('.ui-dialog').css({
position:'fixed',
top:Math.round(($(window).height() - Math.min($(window).height(), $('.ui-dialog.ui-widget').height())) / 2) + 'px'
});
_this.$dButtons = $dlg.parent().find('.ui-dialog-buttonpane').find('button');
$submitButton = _this.$dButtons.eq(0);
$submitButton.specialButton('proceed')/*.button({ disabled: true })*/;
_this.$dButtons.eq(1).specialButton('cancel');
$submitButton.submitOnEnter = function(e) {
if (13 === e.which && !this.button('option', 'disabled')) this.click();
};
}
});
testCORS(function(r) {
$CORSStatus.text($CORSStatus.text() + r);
if ('OK' === r) $CORSStatus.hide();
});
var _parseDistroList = function(cb) {
// First, fetch the rendering for this list
// (we can't just use the wikitext because there are parser functions inside)
var listName = $recipsSel.val(),
listTitle = gmd.distroLists[listName].title,
listContent = gmd.distroLists[listName].revisions[0]['*'];
if (!$settingsDefaultL.val() || !gmd.i18nStore || !gmd.i18nStore[$settingsDefaultL.val()]) return alert("No (default) translation available!");
// The current distribution list
gmd.distroList = [];
gmd.currentListTitle = listTitle;
gmd._parseMeta(listTitle, function(r) {
var $distroList = $($.parseHTML(r.parse.text['*'])),
defaultLang = $settingsDefaultL.val(),
useDefault = $settingsMissingI18n.val();
gmd.log("PARSEMETA");
$distroList.find('li').each(function(i, el) {
var $el = $(el),
t = $el.text(),
m = t.match(/(.+)?\s{1,}\[at\]\s*(.+)/);
if (!m) return;
var title = m[1],
project = m[2];
if (!(project in gmd.sitematrix)) project = 'www.' + project;
var to = gmd.sitematrix[project],
lang = project.match(/^[^.]+/);
if (!lang || !to) return;
var isAvailable = lang[0] in gmd.i18nStore,
actualLang = isAvailable ? lang[0] : defaultLang;
if (!isAvailable && (useDefault === 'dontDistribute') && !to.specialcode) return;
//if (!(lang in gmd.knownLangCodes)) return;
var distroItem = {
to: gmd.sitematrix[project],
tolink: $el.find('a'),
title: title,
lang: actualLang,
msg: gmd.i18nStore[actualLang]
};
gmd.distroList.push(distroItem);
});
gmd.secureCall(cb);
});
};
var _sanitizeText = function(t) {
// TODO: Regexp replace option in UI
t = t.replace(/<!--.*?-->/, '');
t = [$msgOptionsPrepend.val(), t, $msgOptionsAppend.val()].join('');
t += "\n\n<small>This message was delivered based on [[:commons:" + gmd.currentListTitle + "]]. Translation fetched from: [[:commons:%listtitle%]] -- ~\~\~\~</small>";
return t;
};
var _send = function() {
_sendRequests();
_sendRequests();
};
var currentMsg = 0;
var _sendRequests = function() {
if ("OK" !== CORSsupported) return alert(CORSsupported);
if (currentMsg >= gmd.distroList.length) {
//alert("Done!");
return;
}
var li = gmd.distroList[currentMsg];
currentMsg++;
// Skip list item if the current checkbox isn't checked
if (!li.$chk[0].checked) return _sendRequests();
doCORSReq({
action: 'tokens'
}, li.to.domain, function(r) {
if (!r || !r.tokens) return gmd.log("NOTOKEN for " + li.to.dbname);
var et = r.tokens.edittoken,
text = '\n' + _sanitizeText(li.msg.revisions[0]['*']).replace('%listtitle%', li.msg.title),
heading = text.match(/(?:<h2>|==)(.+)(?:<h2\/>|==)/);
if (heading) heading = $.trim(heading[1]);
var edit = {
action: 'edit',
title: li.title,
appendtext: text,
summary: '/* ' + heading + ' */ ' + $msgOptionsEditSummary.val(),
nocreate: 1,
redirect: 1,
token: et
};
li.$li.attr('class', 'gmd-doing');
var _success = function(r) {
// Ratelimit errors often happen, so catch them
if (r.error && 'ratelimited' === r.error.code) {
li.$li.attr('class', 'gmd-ratelimt');
setTimeout(function() {
_doRequest(edit);
}, 65000);
} else if (r.edit && r.edit.result === 'Success') {
li.$li.attr('class', 'gmd-done');
_sendRequests();
} else {
li.$li.attr('class', 'gmd-unknown');
$('<span>').text(" Result: " + (r.error ? (r.error.code + r.error.info) : '') + (r.result || '')).appendTo(li.$li);
_sendRequests();
}
};
var _doRequest = function(params) {
doCORSReq(params, li.to.domain, _success, function() {
li.$li.attr('class', 'gmd-failed');
gmd.log("-----> Edit error for " + li.to.dbname);
_sendRequests();
}, 'POST');
};
_doRequest(edit);
}, function() {
_sendRequests();
gmd.log("NOTOKEN for " + li.to.dbname);
}, 'GET');
};
},
_parseMeta: function(pg, cb) {
this.queryAPI({
action: 'parse',
page: pg,
prop: 'text'
}, cb, this.sitematrix['commons.wikimedia.org'].api, 'GET', true);
},
_gotDistributionList: function($select, r) {
gmd.distroLists = {};
$.each(r.query.pages, function(id, pg) {
var t = pg.title.replace(gmd.config.distroPrefix, '/');
gmd.distroLists[t] = pg;
$('<option>').text(t).appendTo($select);
});
},
_gotTranslations: function($select, $status, cb, r) {
var i = 0,
cl = mw.config.get('wgContentLanguage');
gmd.i18nStore = {};
if (!r.query) {
$status.text("The specified message location does not contain sub-pages.");
return;
}
$select.children().remove();
$.each(r.query.pages, function(id, pg) {
var t = pg.title.replace(gmd.currentI18nStore.pagename, '').split('-')[0],
$opt;
if ( !(t in gmd.knownLangCodes) ) return;
++i;
gmd.i18nStore[t] = pg;
$opt = $('<option>').text(t);
if (t === cl) $opt.attr('selected', 'selected');
$opt.appendTo($select);
});
if (i) {
$select.removeAttr('disabled');
} else {
$select.attr('disabled', 'disabled');
}
if ($.isFunction(cb)) cb();
},
/**
** Does a MediaWiki API request and passes the result to the supplied callback.
**/
queryAPI: function (params, callback, url, method, cache) {
return mw.libs.commons.api.query(params,
{
cache: !!cache,
url: url,
method: method,
cb: function(r) {
gmd.secureCall(callback, r);
},
// r-result, query, text
errCb: function(t, r, q) {
gmd.fail(t);
}
});
},
log: function(args) {
var a = Array.prototype.slice.call(arguments, 0);
a.unshift('GMP> ');
if (window.console && $.isFunction(window.console.log)) window.console.log.apply(window.console, a);
},
/**
** Method to catch errors and report where they occurred
**/
secureCall: function (fn) {
var o = gmd;
try {
o.currentTask = arguments[0];
if ($.isFunction(fn)) {
if (fn.name) o.log(fn);
return fn.apply(o, Array.prototype.slice.call(arguments, 1)); // arguments is not of type array so we can't just write arguments.slice
} else if ('string' === typeof fn) {
o.log(fn);
return o[fn].apply(o, Array.prototype.slice.call(arguments, 1)); // arguments is not of type array so we can't just write arguments.slice
} else {
o.log('This is not a function!');
}
} catch (ex) {
o.log('failure at ' + fn, ex);
o.fail(ex);
}
},
fail: function (err, cb) {
},
namspaceInfo: function(pgn) {
var rxNs = pgn.match(/^(\w+)\:.+/),
r,
tNs,
nsId;
if (rxNs && rxNs[1]) {
tNs = rxNs[1];
nsId = tNs.replace(/ /g, '_').toLowerCase();
nsId = mw.config.get('wgNamespaceIds')[nsId];
if (nsId) {
rxNs = new RegExp('^' + mw.RegExp.escape(rxNs[1]) + '\\:', 'i');
r = pgn.replace(rxNs, '');
} else {
nsId = undefined;
}
}
return { ns: nsId, title: r, nsn: tNs || '', pagename: pgn };
}
};
// Expose globally
window.GlobalMessageDelivery = gmd;
// Fire off the rest as soon as the dom is ready
mw.loader.using(['jquery.ui', 'mediawiki.util', 'ext.gadget.libJQuery', 'ext.gadget.libAPI', 'mediawiki.RegExp'], function() {
$( document ).ready( gmd.install );
});
}( jQuery, mediaWiki ));