UserTags/Extending

This is the developer documentation for people interested in coding new modules to extend UserTags. This information is not relevant to users.
== Introduction ==
You should read the user documentation first, make sure you understand the difference between a group and a tag. UserTags modules are straightforward to create, modules are objects that implement the following interface: 
UserTagsJS.extensions.MyModule = {
	start: function(config, username, logger, lang) {
		// Start up code
		// Return Values:
		//	[Array of group strings]
		//	A jQuery promise
		//	An object literal: { tags: {}, groups: [], promise: $.Deferred().promise(), ajax: [{}] }
		//	undefined/null/false/0/''
	},
	generate: function(json1, json2, json3, ...) {
		// AJAX completion callback
		// Return Values:
		//	[Groups Array]
		//	Object literal: { tags: {}, groups: [] }
		//	undefined/null/false/0/''
	},
	generateFailed: function() {
		// AJAX failure callback
		// Return values are the same as generate()
	},
	derive: function(groups) {
		// Derivation function, you receive a HASH containing all generated
		// groups. You can examine the hash to create derivative groups.
		// Return values:
		//	[Array of new groups]
		//	undefined/null/false/0/''
	},
	filter: function(groups) {
		// Filtering function for removing groups
		// Return values:
		//	[groups to remove array]
		//	undefined/null/false/0/''
	}
};
All functions are optional, although you do need to use start() in order for generate() and generateFailed() to be called. Modules that have just start(), filter(), or derive() are fine.
Modules are installed by adding them to the extensions field of the configuration block. The UserTagsJS configuration block actually has more fields than are mentioned in the user documentation since they aren't relevant there:
Field 	Description
tags 	The user defined groups and the associated tag data.
modules 	The user's module configuration data
oasisPlaceBefore	The jQuery selector string for where exactly to put the tags in Oasis.
extensions 	The module objects that are to be loaded by the script.
debug 	Boolean flag to enable debug mode. Exposes the final state in the UserTagsJS global, prints a lot of debug messages and exposes the listActiveModules function on the UserTagsJS global to allow you to query which modules are causing the script to lock-up due to an unfulfilled promise.
loader 	This is an object created by the UserTags loader. It holds some basic initial state generated by the loader (user and the wiki skin names).
== Interface ==
=== start(config, username, logger, lang) ===
Parameter	Details
config 	The configuration data supplied by the user in UserTagsJS.modules.ModuleName.
username 	The username of the user that is being tagged.
logger 	A logger object for recording debug information and error messages.
lang 	The code for the language being used, use this to decide what language to use on tags.
The start function is the main function for a module, it receives the user configuration and decides what the module is going to do. It starts any internal AJAX if needed and returns an information block.
The primary return value is an object literal with the following fields (all fields are optional):
Field 	Type 	Description
groups 	Array 	An array of groups to add to the user as mentioned
tags 	Object map 	Map of groups to tag data. This has the exact same format as the tags parameter in the UserTagsJS configuration block (see the user documentation) with the exception that you can add a weight field to bias for/against your provided tag data. Please keep translating/localising in mind, use the lang parameter to decide what language to display on the tags you return here.
promise	jQuery Promise 	The existence of this field indicates that asynchronous work needs to be performed. The core will wait for the promise to resolve or reject before it moves to the final filtering pass.
ajax 	Array of literals / Single literal	An AJAX descriptor or array of AJAX descriptors. See Using AJAX
The function may instead return an array of groups or a promise directly instead of the above literal, these will be interpreted as { groups: [array] } and { promise: promise } respectively, and are functionally equivalent. If you return null/false/undefined/etc or an empty object literal then the core will consider that "generic success" and will ignore the module until the derivation and filtering pass (i.e. only the derive/filter functions will be called later, no other effects occur).
==== Resolving the Promise ====
When the promise you provide is resolved (if you provided one) then you should resolve it with the same values as per the return values for generate() and generateFailed(). When the promise is rejected, the value you reject it with does not matter.
==== Tags Reference ====
These are the fields in the tags object map:
Parameter	Description 	Type 	Default
u 	The text to display when the [tagged] user has an unknown gender (not set in their options). 	Text 	Required
m 	The text to display when the [tagged] user is male 	Text 	If missing, the u value will be used instead
f 	The text to display when the [tagged] user is female 	Text 	If missing, the u value will be used instead
order 	Controls the order of tags in the row. Lower numbers are placed at the start and the biggest number at the end. 	Number	10100
link 	Allows you to turn the tag into a link to a page either on the Wiki or anywhere on the Internet. It accepts normal wiki links like "Project:Administrators" or full URLs like "http://www.google.com/". If this is not set then the tag will be displayed as plain text instead. Be aware that interwiki links (like "Wikipedia:Computer") will not work, use a full http:// URL instead.	Text 	blank
title 	Sets the text displayed when the user hovers their mouse over the tag. 	Text 	blank
weight 	If multiple modules define tags for the same group then the weight will be used to decide which one "wins". Generally, you shouldn't set this field unless you have a good reason to believe your tag data is more accurate than usual. Larger values are considered more important then smaller/negative ones. 	Number	0
alias 	WIP. Pulls all of the fields (except weight) from a different groups' tag. This field is a string holding a group name, that group's tag will be substituted as the contents of the tag using the alias parameter.
[Infinite cycles are detected and prevented with an error message] 	String	blank
group 	Proposed (Not implemented). Allows tags to be assigned membership in a group. Each group receives a separate tag-container and can be positioned in the masthead separately. The default group will be placed in the top heading as per normal, but additional groups may be positioned elsewhere, such as immediately below the heading in the main masthead body, or at the bottom. 	String	blank
return {
	tags: {
		group: { u: 'My Group Tag', order: 1 },
		group2: { u: 'My Group 2 Tag', link: 'Project:Policy', order: 2 }
	}
};
=== generate(json...) ===
If start() returned an AJAX descriptor then the core will perform the requested AJAX and call this function with the resulting JSON. If you provided multiple AJAX descriptors as an array then the core will give you several separate JSON objects, one for each descriptor, as separate parameters to the function in the order they were specified in the array. [Be aware that the JSON objects you receive may contain more (but never less) data than you asked for, you may also get the same JSON response object in multiple parameters due to the internal AJAX merging, but this is not guaranteed as merging depends on the user's configuration]
Do not try to modify the JSON parameters. The data is frozen and will cause your module to crash in Strict Mode (in non-strict mode, it will fail silently). The JSON is shared amongst all modules, modifying it will corrupt it; you must clone it if you really need to edit it for some reason.
The return value should be an object literal with the following fields (all fields are optional):
Field 	Type 	Description
groups	Array 	An array of groups to add to the user, same as the literal returned by start()
tags 	Object map	A map of groups to tag data, same as the literal returned by start()
Like start(), you can return an array which will be interpreted as { groups: [array] }. You can also return undefined/null/etc which is the same as returning an empty object literal (i.e. no effect, the core will just move to the filtering pass).
=== generateFailed() ===
This function is called if any of the AJAX you wanted have failed. You should only implement this function if you need to cancel your own internal AJAX, or if you can actually recover from the fault. It has no parameters but returns the same results as generate().
=== derive(groups) ===
This is a 3rd generation pass that occurs right before the filtering pass. You are given the final group set (the exact same set as the filter function below) which you can then interpret to generate additional groups which are derived from some combination of the existing ones.
The purpose of this function is so a module can detect a combination, like 'bureaucrat' + 'inactive', and generate a special 'bureaucrat-is-inactive' combined group or similar. [You will need a filter() function to actuallly remove the groups you use to make your derivation, if necessary]
=== filter(groups) ===
This function is called during the filtering pass when UserTags is removing groups before it starts generating the tags. This function returns an array of groups to be removed. The groups parameter the function receives is an object hash of all the groups that were generated by every module, this hash is not reduced at any point so the order that the filter functions were called does not matter (if another module already removed group X, group X will still be in the hash).
Do not try to modify the groups parameter. It's frozen.
== Logging Object ==
The logging object you receive in the logger parameter has the following interface:
Function	Parameters 	Description
log 	Arbitrary (as per console.log)	Creates a generic "log" level message in the user's Web Console.
inf 	Arbitrary (as per console.log)	Creates an "info" level message in the user's Web Console. Use this for unimportant messages like tracing and dumping parameter values.
wrn 	Arbitrary (as per console.log)	Creates a "warn" level message in the user's Web Console. Use this for things which are clearly wrong but not fatal and can be recovered from.
err 	Arbitrary (as per console.log)	Creates an "error" level message in the user's Web Console. Use this for failure problems, like if your internal AJAX requests failed.
group 	boolean 	Opens a message group in the console. If the optional boolean is true then the group will be collapsed by default.
groupEnd	 	Closes the group opened by group()
Please avoid filling the console with noise. You should only log warnings and errors by default, add a debug configuration option that defaults to false to disable any other messages unless explicitly enabled.
== Using AJAX ==
UserTags provides an internal AJAX engine. You should take advantage of this as much as physically possible as the core engine will merge requests from multiple modules into a single request which is much faster than if you issue your own AJAX separately. This engine only works with action=query requests though, if you require a different api.php request then you will have to do that manually.
Accessing the AJAX engine is done via the ajax field in the return value from your start() function. The requests you place in there will be added to the internal pool and the results will be returned to your generate() function when the AJAX is done. The format of the AJAX descriptors is identical to the data field of the object literal taken by jQuery's $.ajax function. 
UserTagsJS.extensions.isblocked = {
	start: function(config, username) {
		return {
			ajax: {
				list: 'blockinfo',
				bkusers: username,
				bkprop: 'expiry',
				bkdir: 'older',
				bklimit: 1
			}
		};
	},
	generate: function(json) {
		json = json.query.blocks[0];
		if (json && Date.now() < Date.parse(json)) {
			return ['blocked'];
		}
	}
};
The important thing to note is that you can only request one type of request at a time. If you want to request multiple data blocks, like list: 'users' as well as blockinfo in the above example then you must use separate AJAX descriptors to do so, attempting to combine multiple requests into a single descriptor will cause an exception. Also make sure that the fields you use are relevant to the request you are making, if you use irrelevant fields then you can break other modules or cause the entire script to abort due a parameter collision.
There are also several special message types that the engine processes specially because they are relatively common and can be merged easily. These special types must be invoked manually using a special request format.
=== action=query&list=users ===
To access user information, use: 
{
	type: 'user',
	prop: ['prop', 'prop']
}
This is equivalent to the generic request for list=users (prop is an array of uspropvalues), except that the properties list will be merged with the properties lists from other modules as well, thereby minimising the number of AJAX requests that need to be performed. This is equivalent to: 
{
	list: 'users',
	ususers: username,
	usprop: 'prop|prop'
}
=== action=query&list=usercontribs ===
{
	type: 'contributions',
	prop: ['prop', 'prop'],
	limit: 1,
	dir: 'newer'
}
This is equivalent to: 
{
	list: 'usercontribs',
	ucuser: username,
	ucprop: 'prop|prop',
	uclimit: 1,
	ucdir: 'newer'
}
=== action=query&meta=allmessages ===
You can retrieve a limited set of messages from the server using this special format. This format is only for retrieving gender specific messages from the servers translation store, like [ group-bureaucrat-member]. 
{
	type: 'usermessage',
	messages: ['a', 'b']
}
This is equivalent to: 
{
	meta: 'allmessages',
	amenableparser: 1,
	amargs: username,
	ammessages: 'a|b'
}
== Example ==
Here's a simple Hello World module that adds a tag saying "Hello" to everyone: 
window.UserTagsJS = { extensions: {}, modules: {}, debug: true };
window.UserTagsJS.extensions.HelloWorld = {
	start: function() {
		return {
			tags: {
				hello: { u: 'Hello' }
			},
			groups: ['hello']
		};
	}
};
window.UserTagsJS.modules.HelloWorld = true; // Enable the module
importScriptPage('UserTags/code.js', 'dev');
We define a new group "hello", we assign a tag containing the relevant text to it and we unconditionally return the 'hello' group for every user. Simple enough. You'll notice that we don't use any configuration but I still set the configuration data to true, this is because the core will not load the module unless the module is enabled (i.e. configured). Setting the configuration to true satisfies that requirement.
An equally simple example of removing the 'sysop' group from every user would be: 
window.UserTagsJS = { extensions: {}, modules: {}, debug: true };
window.UserTagsJS.extensions.RemoveSysop = {
	filter: function() {
		return ['sysop'];
	}
};
window.UserTagsJS.modules.RemoveSysop = true;
window.UserTagsJS.modules.mwGroups = ['bureaucrat', 'sysop', 'autoconfirmed'];
importScriptPage('UserTags/code.js', 'dev');
If you've read the Using AJAX section then you'll already have a reasonable idea how AJAX works, but what if you want to do your own custom AJAX? Things get somewhat more complicated. Let's try adding a custom tag to users who have over 100 edits in the main article namespace. We'll do this using Special:Editcount and the Parsing API: 
window.UserTagsJS = { extensions: {}, modules: {}, debug: true };
window.UserTagsJS.extensions.Over100Edits = {
	start: function(config, username) {
		var promise = $.ajax({
			url: mw.util.wikiScript('api'),
			data: {
				action: 'parse',
				format: 'json',
				text: '{{Special:Editcount/' + username + '/0}}',
				prop: 'text',
				disablepp: 1
			},
			dataType: 'json'
		}).then(function(json) {
			var num = $(json.parse.text['*']).text().replace(/[^\d]/g, );
			if (num && +num > 100) {
				return ['edit100'];
			}
			return null;
		});
		return {
			tags: {
				edit100: { u: '100+ Article Edits!' }
			},
			promise: promise
		};
	}
};
window.UserTagsJS.modules.Over100Edits = true; // Enable the module
importScriptPage('UserTags/code.js', 'dev');
Hopefully you get the idea. You can also look at the built-in modules for more examples.
== Debugging ==
You can run UserTags a second (you must use /code.js the first time to initialise the UserTagsJS.loadermember) or more times without reloading your test page using the following mini-script: 
window.loadedScripts = {};
delete dev.UserTags;
importScriptPage('UserTags/core.js', 'dev');
You must have the debug configuration option in UserTagsJS set to true for this to work; otherwise the UserTagsJS global will be deleted after each run which will cause the core to abort with a "bad loader" error. The debug option is also required to expose the listActiveModules function that will help you figure out why the script is stuck if you are using promises in your design. There is also the TagData member that holds all the tag data received from every module and the user configuration, this can be useful in figuring out why you are getting garbage output.
Be aware that resetting the loadedScripts global may have bad consequences so don't use the tab to do anything important until you refresh it.
== Deployment ==
You should package your modules into a JS file with the following boilerplate: 
window.UserTagsJS = $.extend(true, window.UserTagsJS, { extensions: {} });
UserTagsJS.extensions.YourModuleName = {
	// Code ...
};
This boilerplate is designed to make sure the global exists and has an extensionsfield for you. Users will then include your module using importArticles: 
// ...
UserTagsJS.modules.YourModuleName = { /* user configuration for you */ };
importArticles({
	type: 'script',
	articles: [
		// ...
		'w:c:dev:User:You/YourModule.js', // Must be first, obviously
		'w:c:dev:UserTags/code.js',
		// ...
	]
});
The important part is that the module must obviously be loaded before UserTags runs.
Category:JavaScript