MediaWiki:Gadget-libNotify.js

From wikishia

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.
/**
 * This file is documented using
 * [jsduck-syntax](https://github.com/senchalabs/jsduck).
 * The source code is available at 
 * [Wikimedia Commons](http://commons.wikimedia.org/wiki/MediaWiki:Gadget-libNotify.js)
 * ([raw](http://commons.wikimedia.org/wiki?title=MediaWiki:Gadget-libNotify.js&action=raw&ctype=text/javascript))
 *
 * @class notify
 * @singleton
 * 
 *
 *
 * Quick start: Simple usage:
 *
        @example 
        // Load the gadget
        mw.loader.using('ext.gadget.libNotify', function () {
            
        });
 *
 *
 * @requires jQuery
 * @requires mediaWiki
 * @requires libAPI
 * @requires wikiDOM
 *
 * @author Rainer Rillke <https://commons.wikimedia.org/wiki/User:Rillke>
 **/
 
/*
 * @license
 *   The MIT License (MIT) [full text see below]
 *   GNU General Public License, version 3 (GPL-3.0)
 *   Creative Commons Attribution 3.0 (CC-BY-3.0)
 * Choose whichever license of these you like best.
 *
 * @jshint valid
 * <http://jshint.com/>
 */
/*jshint curly:false, smarttabs:true*/

/*!
	Copyright (c) 2013 Rainer Rillke <https://commons.wikimedia.org/wiki/User:Rillke>

	Permission is hereby granted, free of charge, to any person obtaining a copy
	of this software and associated documentation files (the "Software"), to deal
	in the Software without restriction, including without limitation the rights
	to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
	copies of the Software, and to permit persons to whom the Software is
	furnished to do so, subject to the following conditions:

	The above copyright notice and this permission notice shall be included in
	all copies or substantial portions of the Software.

	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
	IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
	AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
	LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
	OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
	THE SOFTWARE.
**/

(function($, mw, undefined) {
	'use strict';
	var n, defaultRecipientSettings;
	
	function firstValue(o) { for (var i in o) { if (o.hasOwnProperty(i)) { return o[i]; } } }
	
	/**
	 * @cfg defaultRecipientSettings
	 *  The default recipient settings can be overwritten by each
	 *  recipient by creating a custom JSON file in the user namespace.
	 * @cfg {Array} [defaultRecipientSettings.optOutTags=[]]
	 *  If the tag of the current message is in the list of opted-out tags,
	 *  the message won't be dispatched to the user.
	 * @cfg {Array} [defaultRecipientSettings.optOutGroups=[]]
	 *  If the group of the current message is in the list of optOutGroups,
	 *  the message won't be dispatched to the user, except the tag is in
	 *  the list of optInTags.
	 * @cfg {Array} [defaultRecipientSettings.optInTags=[]]
	 *  If set, and both the tag and the group are not opted-in,
	 *  the message won't be dispatched to the user.
	 * @cfg {Array} [defaultRecipientSettings.optInGroups=[]]
	 *  If set, and both the tag and the group are not opted-in,
	 *  the message won't be dispatched to the user.
	 * @cfg {String|string} [defaultRecipientSettings.sendCopyTo='']
	 *  Project to which to dispatch the message.
	 *  Requirements: defaultMessge.followIWRedir
	 * @cfg {boolean} [defaultRecipientSettings.minor=false]
	 *  Whether to mark notifications as minor or not.
	 *  Only respected if not specified by message.
	 */
	/**
	 * @property {Object} defaultRecipientSettings
	 *  Confer to ``defaultRecipientSettings`` configuration.
	 * @private
	 */
	defaultRecipientSettings = {
		optOutTags: [],
		optOutGroups: [],
		optInTags: [],
		optInGroups: [],
		sendCopyTo: '',
		minor: false
	};
	
	n = mw.libs.notify = {
		/**
		 * @cfg defaultMessge
		 *  A default message.
		 * @cfg {String|string} defaultMessge.username
		 *  User name without namespace. E.g. 'Rillke' not 'User:Rillke'
		 * @cfg {String|string} [defaultMessge.project=location.host]
		 *  The project where the user should be notified.
		 *  Note that either CORS is used, or in future, possibly OAuth.
		 * @cfg {String|string} [defaultMessge.tag='']
		 *  The tag or a short form of the message that is used for notifying
		 *  the user. This is to allow the addressee opting-out specific messages.
		 * @cfg {String|string} [defaultMessge.group='']
		 *  The type/group this message belongs to.
		 *   This is to allow the addressee opting-out messages groups.
		 * @cfg {String|string} defaultMessge.summary
		 *  The edit-summary used.
		 * @cfg {String|string} defaultMessge.text
		 *  The text to be appended to the user talk page.
		 * @cfg {boolean} [defaultMessge.followIWRedir=false]
		 *  Whether to follow inter-wiki redirects (if project is the default). 
		 *  This only works if the sender is logged-in globally and
		 *  allows cross-domain communication and both accounts are connected
		 *  through CentralAuth (SUL).
		 *  notifyUploader will try to guess the language of the target
		 *  project and expand all templates used in the text property in that
		 *  language and in the context of the Commons- User talkpage.
		 * @cfg {Array} [defaultMessge.writePermission=['autoconfirmed']]
		 *  Write permission to assume for the current account.
		 * @cfg {String|string} [defaultMessge.watchlist='watch']
		 *  Whether to watch the user talk page. One of the following values:
		 *  watch, unwatch, preferences, nochange
		 */
		/**
		 * @property {Object} defaultMessge
		 *  Confer to ``defaultMessge`` configuration.
		 */
		defaultMessge: {
			username: '',
			project: location.host,
			tag: '',
			group: '',
			summary: "",
			text: "",
			followIWRedir: false,
			// You may set this to  mw.config.get('wgUserGroups')
			// to use your sysop privileges to notify everyone
			writePermission: ['autoconfirmed'],
			watchlist: 'watch'
		},

		/**
		 * @property {String|string} welcomeTemplate
		 *  Template that is automatically added if the user talk page
		 *  does not yet exist.
		 */
		welcomeTemplate: '{{welcome}}\n',

		
		/**
		 * Notifies multiple users.
		 *
		 * @param {Array} messages
		 *  An Array of objects matching the pattern described at
		 *  the ``defaultMessge`` config.
		 *
		 * @return {$.Promise} 
		 *  A jQuery Promise that is resolved after all
		 *  users were notified, that is rejected if one user
		 *  could unexpectedly not be notified and that emits
		 *  progress messages after one user was notified.
		 *  The promis is augmented by
		 *  ``.total``: The total number of edits
		 *  ``.all``: An Array containing jqXHRs
		 *  ``.pending()``: Returns the number of pending edits
		 *
             @example 
        // Load the gadget
        mw.loader.using('ext.gadget.libNotify', function () {
             var $p = mw.libs.notify.$users([{
                 username: 'Rillke',
                 tag: 'Welcome',
                 group: 'info',
                 summary: "Welcome 1! (Live-Testing new notifyer script)",
                 text: "\n{{welcome}}",
                 watchlist: 'watch'
             },{
                 username: 'RillkeBotT',
                 tag: 'Welcome',
                 group: 'info',
                 summary: "Welcome 2! (Live-Testing new notifyer script)",
                 text: "\n{{welcome}}",
                 watchlist: 'watch'
             },{
                 username: 'RillkeBot',
                 tag: 'Welcome',
                 group: 'info',
                 summary: "Welcome 3! (Live-Testing new notifyer script)",
                 text: "\n{{welcome}}",
                 watchlist: 'watch'
             }]).done(function() {
                 console.log('done');
             }).fail(function() {
                 console.log('err');
             }).progress(function() {
                 console.log('p', $p.total, $p.pending(), $p.justNotified);
             });
         });
		 *
		 */
		$users: function(messages) {
			var $prom, $proms;
			
			$proms = $.map(messages, function(msg) {
				return n.$user(msg);
			});
			$prom = $.when.apply($.when, $proms);
			$.extend($prom, {
				total: $proms.length,
				all: $proms,
				pending: function() {
					var p = 0;
					$.each($proms, function(i, $sp) {
						if ('pending' === $sp.state()) p++;
					});
					return p;
				},
				justNotified: '',
				knownUsers: {}
			}).progress(function(/*args*/) {
				var jn = [], u;
				$.each(arguments, function(i, arg) {
					if (arg && arg[0] && arg[0].user) {
						u = arg[0].user;
						if (!(u in $prom.knownUsers)) {
							$prom.knownUsers[u] = 1;
							jn.push(u);
						}
					}
				});
				$prom.justNotified = jn.join('\n') || $prom.justNotified;
			});
			return $prom; // .then(ok, fail)
		},

		/**
		 * Notifies one single user.
		 *
		 * @param {Object} message
		 *  Objects matching the pattern described at
		 *  the ``defaultMessge`` config.
		 *
		 * @return {$.Promise} 
		 *  A jQuery Promise that is resolved after all
		 *  users were notified, that is rejected if one user
		 *  could unexpectedly not be notified and that emits
		 *  progress messages after one user was notified.
		 */
		$user: function(message) {
			message = $.extend({}, n.defaultMessge, message);

			var isLocal = message.project === location.host,
				normalizedUserName = mw.libs.wikiDOM.normalizeTitle(message.username),
				// MediaWiki redirects to the localized version
				userPage = 'User:' + normalizedUserName,
				userTalk = 'User talk:' + normalizedUserName,
				settingsPage, __isSet, __isIn;
				
			if (isLocal) {
				userPage = mw.libs.wikiDOM.getLocalizedNamespace('user') + ':' + normalizedUserName;
				userTalk = mw.libs.wikiDOM.getLocalizedNamespace('user_talk') + ':' + normalizedUserName;
			}
			settingsPage = userPage + '/Notification Settings.js';
			
					
			if (!isLocal) throw new Error('Notifying users in sister projects is not implemented yet!');

			__isSet = function(prop) {
				return $.isArray(prop) && prop.length;
			};
			__isIn = function(msgKey, prop) {
				return $.inArray(message[msgKey], prop || []) > -1;
			};
			
			return $.get(mw.util.wikiScript() + '?' + $.param({
				title: settingsPage,
				action: 'raw',
				ctype: 'text/javascript',
				maxage: 86400,
				smaxage: 86400
			})).then(function(r) {
				// Got recipient's preferences
				var skip;

				r = $.trim(r) || '{}';
				r = JSON.parse(r);
				r = $.extend({}, defaultRecipientSettings, r);
				if ( !('minor' in message) ) message.minor = r.minor;
				
				if (__isIn('group', r.optOutGroups)) {
					skip = true;
				}
				if (__isSet(r.optInGroups) || __isSet(r.optInTags)) {
					if (!__isIn('group', r.optInGroups) && !__isIn('tag', r.optInTags)) {
						// User has specified opt-in-groups but this message is not in
						skip = true;
					} else {
						skip = false;
					}
				}
				if (__isIn('tag', r.optOutTags)) {
					skip = true;
				}
				
				// sendCopyTo -- not implemented yet
				// this should be not chained anyway;
				// not that important to stop on any error thrown by
				// other projects; however an autoreport could be sent
				
				if (!skip) {
					return mw.libs.commons.api.$query({
							action: 'query',
							titles: userTalk,
							prop: 'info',
							inprop: 'protection',
							intoken: 'edit'
					});
				}
			}).then(function(r) {
				// Got protection level, possibly dispatch message
				var text = message.text,
					skip, pg, $prom;
				
				if (!r) return; // previously skipped
				pg = firstValue(r.query.pages);
				$.each(pg.protection, function(i, x) {
					if (x.type === 'edit') {
						if ($.inArray(x.level, message.writePermission) === -1) {
							skip = true;
							return false;
						}
					}
				});
				if ('missing' in pg) {
					text = n.welcomeTemplate + text;
				}
				if (!skip) {
					$prom = mw.libs.commons.api.$editPage({
						editType: 'appendtext',
						title: userTalk,
						text: text,
						summary: message.summary,
						minor: message.minor,
						watchlist: message.watchlist,
						redirect: true,
						token: pg.edittoken
					}).progress(function(r) {
						if (r) r.user = normalizedUserName;
					});
					return $prom;
				}
			});
		}
	};
	
}(jQuery, mediaWiki));
//</nowiki>