<commit_before>// JavaScript plugin example for Choob.
// 
// Copyright 2005 - 2006, <NAME>
// 

var BufferedReader    = Packages.java.io.BufferedReader;
var File              = Packages.java.io.File;
var FileInputStream   = Packages.java.io.FileInputStream;
var InputStreamReader = Packages.java.io.InputStreamReader;
var System            = Packages.java.lang.System;
var URL               = Packages.java.net.URL;
var ChoobPermission   = Packages.uk.co.uwcs.choob.support.ChoobPermission;
var GetContentsCached = Packages.uk.co.uwcs.choob.support.GetContentsCached;

// Constructor: JSExample
function JSExample(mods, irc) {
	// Set off two callbacks.
	// Parameters are:
	//   <param>  Argument passed directly to the callback. Used to identify
	//            different intervals in this plugin.
	//   <delay>  Miliseconds to wait before callback.
	//   <id>     A unique ID for the callback. Setting a callback with the same
	//            ID as an existing one will replace it. IDs are released once
	//            once the callback has been made.
	mods.interval.callBack("init", 1000, 1);
	mods.interval.callBack("trac-svn", 15000, 2);
	this.mods = mods;
	
	this._debugChannel = "#testing42";
	this._announceChannel = "#bots";
	
	/*var url = new URL("http://trac.warwickcompsoc.co.uk/choob/timeline?daysback=30&max=5&wiki=on&ticket=on&changeset=on&milestone=on&format=rss");
	this._tracRSS = new GetContentsCached(url, 30000);
	this._tracRSSLastItem = "";
	this._tracRSSChannel = this._announceChannel;*/
	
	var url2 = new URL("http://trac.warwickcompsoc.co.uk/choob/roadmap");
	this._tracRoadmap = new GetContentsCached(url2, 30000);
	this._tracRoadmapLastData = new Object();
	this._tracRoadmapChannel = this._announceChannel;
}


JSExample.prototype.info = [
		"Basic example JavaScript plugin w/ Trac Roadmap notifications.",
		"<NAME>",
		"<EMAIL>",
		"1.3.0"
	];


// Callback for all intervals from this plugin.
JSExample.prototype.interval = function(param, mods, irc) {
	// We use the <param> to identify the callback function to call. This has
	// some significant advantages over the basic interval/callback model
	// provided for us. The main one is, of course, that we keep the code for
	// each interval (however many we have) separate and self-contained.
	// 
	// To do this, we construct a method name from <param>, by adding a "_"
	// prefix (methods that don't start with a letter are ignored by the
	// plugin manager), then removing all "-"s, upper-casing the following
	// letter. This gives the nice mapping of a name "foo-bar-baz" to method
	// _fooBarBaz. We call the method with the same signature as this
	// function, and announce any errors from this code in the 'debug channel'.
	
	function mapCase(s, a, b) {
		return a.toUpperCase() + b.toLowerCase();
	};
	
	if (param) {
		var name = "_" + param.replace(/-(\w)(\w+)/g, mapCase) + "Interval";
		
		if (name in this) {
			this[name](param, mods, irc);
		} else {
			irc.sendMessage(this._debugChannel, "Interval code missing: " + name);
		}
		
	} else {
		irc.sendMessage(this._debugChannel, "Unnamed interval attempted!");
	}
}


// Command: Exit
JSExample.prototype.commandExit = function(mes, mods, irc) {
	if (!mods.security.hasPerm(new ChoobPermission("exit"), mes)) {
		irc.sendContextReply(mes, "You don't have permission to do that!");
		return;
	}
	var params = mods.util.getParams(mes, 1);
	if (params.size() <= 1) {
		irc.quit("Bye bye!");
	} else {
		irc.quit(params.get(1));
	}
}
JSExample.prototype.commandExit.help = [
		"Makes the bot exit, if you have permission."
	];


// Command: Restart
JSExample.prototype.commandRestart = function(mes, mods, irc) {
	if (!mods.security.hasPerm(new ChoobPermission("exit"), mes)) {
		irc.sendContextReply(mes, "You don't have permission to do that!");
		return;
	}
	var params = mods.util.getParams(mes, 1);
	if (params.size() <= 1) {
		irc.restart("Restarting...");
	} else {
		irc.restart(params.get(1));
	}
}
JSExample.prototype.commandRestart.help = [
		"Makes the bot exit, recompile, and restart itself, if you have permission."
	];


// Command: Info
JSExample.prototype.commandInfo = function(mes, mods, irc) {
	try {
		var version = this._getSVNRevision();
		if (version) {
			irc.sendContextReply(mes, "Currently running SVN revision " + version + ".");
		} else {
			irc.sendContextReply(mes, "Couldn't find a version in the SVN file!");
		}
	} catch(ex) {
		irc.sendContextReply(mes, "Error getting SVN info: " + ex);
	}
}
JSExample.prototype.commandInfo.help = [
		"Displays some information about the bot."
	];


// Command: Roadmap
JSExample.prototype.commandRoadmap = function(mes, mods, irc) {
	var ok = false;
	
	for (var i in this._tracRoadmapLastData) {
		var itemObj = this._tracRoadmapLastData[i];
		ok = true;
		
		irc.sendContextReply(mes, "Trac: '" + itemObj.name + "' is " +
				itemObj.percent + "% complete (" +
				itemObj.ticketsOpen + " of " +
				itemObj.ticketsTotal + " tickets remain).");
	}
	if (!ok) {
		irc.sendContextReply(mes, "Either there are no Trac releases, or there was a problem getting the data.");
	}
}
JSExample.prototype.commandRoadmap.help = [
		"Shows the current Trac roadmap information."
	];


// Command: Timeline
JSExample.prototype.commandTimeline = function(mes, mods, irc) {
	var items = this._getTracRoadmapItems(5, null);
	
	if (items.length == 0) {
		irc.sendContextReply(mes, "Either there are no Trac timeline items, or there was a problem getting the data.");
		return;
	}
	for (var i = 0; i < items.length; i++) {
		irc.sendContextReply(mes, "Trac: " + items[i].title + " (" + items[i].link + ") - " + items[i].desc);
	}
}
JSExample.prototype.commandTimeline.help = [
		"Shows the newest 5 items from the Trac timeline."
	];


// Command: Eval
JSExample.prototype.commandEval = function(mes, mods, irc) {
	if (!mods.security.hasPerm(new ChoobPermission("admin.eval"), mes)) {
		irc.sendContextReply(mes, "You don't have permission to do that!");
		return;
	}
	
	var params = mods.util.getParams(mes, 1);
	if (params.size() <= 1) {
		irc.sendContextReply(mes, "Syntax: JSExample.Eval <JS>");
		return;
	}
	var js = "" + params.get(1);
	
	try {
		var rv = "" + eval(js);
	} catch(ex) {
		if (("name" in ex) && ("message" in ex) && ("fileName" in ex)) {
			irc.sendContextReply(mes, "Exception: [" + ex.name + "] <" + ex.message + "> at <" + ex.fileName + ">.");
			if (ex.javaException) {
				ex.javaException.printStackTrace();
			}
		} else {
			irc.sendContextReply(mes, "Exception: " + ex);
		}
		return;
	}
	rv = rv.replace(/[\r\n]+/g, " ");
	
	irc.sendContextReply(mes, "Result: " + rv);
}
JSExample.prototype.commandTimeline.help = [
		"Shows the newest 5 items from the Trac timeline."
	];


// API success
JSExample.prototype.apiSuccess = function() {
	return "JSExample API Success call";
}

// API failure
JSExample.prototype.apiFailure = function() {
	throw new Error("JSExample API Failure call");
}

// API status (test to match NickServ)
JSExample.prototype.apiStatus = function() {
	return "JSExample API Status call";
}

// API callBack test
JSExample.prototype.apiCallback = function(text) {
	this._apiCBTest = text;
	this.mods.interval.callBack("api-test", 1000, 1);
}

// API to call another API
JSExample.prototype.apiCallAPI = function(plugin, api, param1, param2) {
	this.mods.security.getPluginNames("JSExample:apiCallAPI");
	this.mods.plugin.callAPI(plugin, api, [param1, param2]);
}

// Interval: api-test
JSExample.prototype._apiTestInterval = function(params, mods, irc) {
	irc.sendMessage(this._debugChannel, "API-test interval result: " + this._apiCBTest);
}

// Interval: init
JSExample.prototype._initInterval = function(param, mods, irc) {
	irc.sendMessage(this._debugChannel, "Interval! Woo!");
}


// Interval: Trac RSS
JSExample.prototype._tracSvnInterval = function(param, mods, irc) {
	try {
		/*var newItems = this._getTracRoadmapItems(5, this._tracRSSLastItem);
		
		if (this._tracRSSLastItem) {
			for (var i = 0; i < newItems.length; i++) {
				irc.sendMessage(this._tracRSSChannel, "Trac: " + newItems[i].title + " (" + newItems[i].link + ") - " + newItems[i].desc);
			}
		}
		if (newItems.length > 0) {
			this._tracRSSLastItem = newItems[newItems.length - 1].date;
		}*/
		
		// Roadmap changes.
		var roadmapData = "" + this._tracRoadmap.getContents();
		roadmapData = roadmapData.replace(/[\r\n]+/g, "");
		
		var firstRun = true;
		var roadmapChan = this._debugChannel;
		for (var i in this._tracRoadmapLastData) {
			firstRun = false;
			roadmapChan = this._tracRoadmapChannel;
			break;
		}
		
		var roadmapRegExp = new RegExp("class=\"milestone\".*?class=\"description\"", "gi");
		
		var item;
		while ((item = roadmapRegExp.exec(roadmapData))) {
			var itemObj = new Object();
			itemObj.name = /<em>(.*?)<\/em>/i.exec(item)[1];
			itemObj.percent = 1 * /class="percent">(.*?)\%</i.exec(item)[1];
			itemObj.ticketsOpen = 1 * /\?status=new[^"]+">(.*?)</i.exec(item)[1];
			itemObj.ticketsClosed = 1 * /\?status=closed[^"]+">(.*?)</i.exec(item)[1];
			itemObj.ticketsTotal = itemObj.ticketsOpen + itemObj.ticketsClosed;
			itemObj.all = [
					itemObj.name, itemObj.percent, itemObj.ticketsOpen, itemObj.ticketsClosed
				].join(",");
			
			var lastObj = null;
			if (this._tracRoadmapLastData && (itemObj.name in this._tracRoadmapLastData)) {
				lastObj = this._tracRoadmapLastData[itemObj.name];
			}
			
			//if (firstRun && !lastObj) {
			//	lastObj = {
			//			name: itemObj.name, percent: 100,
			//			ticketsOpen: 0, ticketsClosed: 0, ticketsTotal: 0,
			//			all: itemObj.name + ",0,0,0"
			//		};
			//}
			
			if (lastObj) {
				if ((lastObj.ticketsOpen > 0) && (itemObj.ticketsOpen == 0) && (itemObj.ticketsTotal > 0)) {
					irc.sendMessage(roadmapChan, "Trac: '" + itemObj.name + "' is now complete (all " +
							itemObj.ticketsTotal + " tickets closed)! :-D");
					
				} else if (itemObj.ticketsOpen < lastObj.ticketsOpen) {
					var count = (lastObj.ticketsOpen - itemObj.ticketsOpen);
					
					irc.sendMessage(roadmapChan, "Trac: '" + itemObj.name + "' has gone from " +
							lastObj.percent + "% complete to " +
							itemObj.percent + "% complete :-) (" +
							count + (count == 1 ? " ticket" : " tickets") + " closed, now " +
							itemObj.ticketsOpen + " of " +
							itemObj.ticketsTotal + (itemObj.ticketsTotal == 1 ? " ticket" : " tickets") + " remain).");
					
				} else if (itemObj.ticketsOpen > lastObj.ticketsOpen) {
					var count = (itemObj.ticketsOpen - lastObj.ticketsOpen);
					
					irc.sendMessage(roadmapChan, "Trac: '" + itemObj.name + "' has gone from " +
							lastObj.percent + "% complete to " +
							itemObj.percent + "% complete :-( (" +
							count + (count == 1 ? " ticket" : " tickets") + " opened, now " +
							itemObj.ticketsOpen + " of " +
							itemObj.ticketsTotal + (itemObj.ticketsTotal == 1 ? " ticket" : " tickets") + " remain).");
				}
				
			} else if (!firstRun) {
				irc.sendMessage(roadmapChan, "Trac: '" + itemObj.name + "' is " +
						itemObj.percent + "% complete (" +
						itemObj.ticketsOpen + " of " +
						itemObj.ticketsTotal + " tickets remain).");
			}
			this._tracRoadmapLastData[itemObj.name] = itemObj;
		}
	} catch (ex) {
		irc.sendMessage(this._debugChannel, "_tracSvnInterval exception: line " + ex.lineNumber + ", " + ex);
	}
	mods.interval.callBack(param, 30000, 2);
}


// Command: JS
JSExample.prototype.commandJS = function(mes, mods, irc) {
	irc.sendContextReply(mes, "JS Rocks!");
}
// Note: This maps internally to the generic helpCommandJS.
//       It is purely convenience (and nice OO).
JSExample.prototype.commandJS.help = ["Says something about JS."];


// Command: Test
JSExample.prototype.commandTest = function(mes, mods, irc) {
	irc.sendContextReply(mes, "Our name: " + mods.security.getPluginName(0));<commit_msg>Fix parsing bug in Trac timeline code. Change interval to back off to 5m if it fails, instead of always being 30s.<commit_after>// JavaScript plugin example for Choob.
// 
// Copyright 2005 - 2006, <NAME>
// 

var BufferedReader    = Packages.java.io.BufferedReader;
var File              = Packages.java.io.File;
var FileInputStream   = Packages.java.io.FileInputStream;
var InputStreamReader = Packages.java.io.InputStreamReader;
var System            = Packages.java.lang.System;
var URL               = Packages.java.net.URL;
var ChoobPermission   = Packages.uk.co.uwcs.choob.support.ChoobPermission;
var GetContentsCached = Packages.uk.co.uwcs.choob.support.GetContentsCached;

// Constructor: JSExample
function JSExample(mods, irc) {
	// Set off two callbacks.
	// Parameters are:
	//   <param>  Argument passed directly to the callback. Used to identify
	//            different intervals in this plugin.
	//   <delay>  Miliseconds to wait before callback.
	//   <id>     A unique ID for the callback. Setting a callback with the same
	//            ID as an existing one will replace it. IDs are released once
	//            once the callback has been made.
	mods.interval.callBack("init", 1000, 1);
	mods.interval.callBack("trac-svn", 15000, 2);
	this.mods = mods;
	
	this._debugChannel = "#testing42";
	this._announceChannel = "#bots";
	
	/*var url = new URL("http://trac.warwickcompsoc.co.uk/choob/timeline?daysback=30&max=5&wiki=on&ticket=on&changeset=on&milestone=on&format=rss");
	this._tracRSS = new GetContentsCached(url, 30000);
	this._tracRSSLastItem = "";
	this._tracRSSChannel = this._announceChannel;*/
	
	var url2 = new URL("http://trac.warwickcompsoc.co.uk/choob/roadmap");
	this._tracRoadmap = new GetContentsCached(url2, 30000);
	this._tracRoadmapLastData = new Object();
	this._tracRoadmapChannel = this._announceChannel;
}


JSExample.prototype.info = [
		"Basic example JavaScript plugin w/ Trac Roadmap notifications.",
		"<NAME>",
		"<EMAIL>",
		"1.3.0"
	];


// Callback for all intervals from this plugin.
JSExample.prototype.interval = function(param, mods, irc) {
	// We use the <param> to identify the callback function to call. This has
	// some significant advantages over the basic interval/callback model
	// provided for us. The main one is, of course, that we keep the code for
	// each interval (however many we have) separate and self-contained.
	// 
	// To do this, we construct a method name from <param>, by adding a "_"
	// prefix (methods that don't start with a letter are ignored by the
	// plugin manager), then removing all "-"s, upper-casing the following
	// letter. This gives the nice mapping of a name "foo-bar-baz" to method
	// _fooBarBaz. We call the method with the same signature as this
	// function, and announce any errors from this code in the 'debug channel'.
	
	function mapCase(s, a, b) {
		return a.toUpperCase() + b.toLowerCase();
	};
	
	if (param) {
		var name = "_" + param.replace(/-(\w)(\w+)/g, mapCase) + "Interval";
		
		if (name in this) {
			this[name](param, mods, irc);
		} else {
			irc.sendMessage(this._debugChannel, "Interval code missing: " + name);
		}
		
	} else {
		irc.sendMessage(this._debugChannel, "Unnamed interval attempted!");
	}
}


// Command: Exit
JSExample.prototype.commandExit = function(mes, mods, irc) {
	if (!mods.security.hasPerm(new ChoobPermission("exit"), mes)) {
		irc.sendContextReply(mes, "You don't have permission to do that!");
		return;
	}
	var params = mods.util.getParams(mes, 1);
	if (params.size() <= 1) {
		irc.quit("Bye bye!");
	} else {
		irc.quit(params.get(1));
	}
}
JSExample.prototype.commandExit.help = [
		"Makes the bot exit, if you have permission."
	];


// Command: Restart
JSExample.prototype.commandRestart = function(mes, mods, irc) {
	if (!mods.security.hasPerm(new ChoobPermission("exit"), mes)) {
		irc.sendContextReply(mes, "You don't have permission to do that!");
		return;
	}
	var params = mods.util.getParams(mes, 1);
	if (params.size() <= 1) {
		irc.restart("Restarting...");
	} else {
		irc.restart(params.get(1));
	}
}
JSExample.prototype.commandRestart.help = [
		"Makes the bot exit, recompile, and restart itself, if you have permission."
	];


// Command: Info
JSExample.prototype.commandInfo = function(mes, mods, irc) {
	try {
		var version = this._getSVNRevision();
		if (version) {
			irc.sendContextReply(mes, "Currently running SVN revision " + version + ".");
		} else {
			irc.sendContextReply(mes, "Couldn't find a version in the SVN file!");
		}
	} catch(ex) {
		irc.sendContextReply(mes, "Error getting SVN info: " + ex);
	}
}
JSExample.prototype.commandInfo.help = [
		"Displays some information about the bot."
	];


// Command: Roadmap
JSExample.prototype.commandRoadmap = function(mes, mods, irc) {
	var ok = false;
	
	for (var i in this._tracRoadmapLastData) {
		var itemObj = this._tracRoadmapLastData[i];
		ok = true;
		
		irc.sendContextReply(mes, "Trac: '" + itemObj.name + "' is " +
				itemObj.percent + "% complete (" +
				itemObj.ticketsOpen + " of " +
				itemObj.ticketsTotal + " tickets remain).");
	}
	if (!ok) {
		irc.sendContextReply(mes, "Either there are no Trac releases, or there was a problem getting the data.");
	}
}
JSExample.prototype.commandRoadmap.help = [
		"Shows the current Trac roadmap information."
	];


// Command: Timeline
JSExample.prototype.commandTimeline = function(mes, mods, irc) {
	var items = this._getTracRoadmapItems(5, null);
	
	if (items.length == 0) {
		irc.sendContextReply(mes, "Either there are no Trac timeline items, or there was a problem getting the data.");
		return;
	}
	for (var i = 0; i < items.length; i++) {
		irc.sendContextReply(mes, "Trac: " + items[i].title + " (" + items[i].link + ") - " + items[i].desc);
	}
}
JSExample.prototype.commandTimeline.help = [
		"Shows the newest 5 items from the Trac timeline."
	];


// Command: Eval
JSExample.prototype.commandEval = function(mes, mods, irc) {
	if (!mods.security.hasPerm(new ChoobPermission("admin.eval"), mes)) {
		irc.sendContextReply(mes, "You don't have permission to do that!");
		return;
	}
	
	var params = mods.util.getParams(mes, 1);
	if (params.size() <= 1) {
		irc.sendContextReply(mes, "Syntax: JSExample.Eval <JS>");
		return;
	}
	var js = "" + params.get(1);
	
	try {
		var rv = "" + eval(js);
	} catch(ex) {
		if (("name" in ex) && ("message" in ex) && ("fileName" in ex)) {
			irc.sendContextReply(mes, "Exception: [" + ex.name + "] <" + ex.message + "> at <" + ex.fileName + ">.");
			if (ex.javaException) {
				ex.javaException.printStackTrace();
			}
		} else {
			irc.sendContextReply(mes, "Exception: " + ex);
		}
		return;
	}
	rv = rv.replace(/[\r\n]+/g, " ");
	
	irc.sendContextReply(mes, "Result: " + rv);
}
JSExample.prototype.commandTimeline.help = [
		"Shows the newest 5 items from the Trac timeline."
	];


// API success
JSExample.prototype.apiSuccess = function() {
	return "JSExample API Success call";
}

// API failure
JSExample.prototype.apiFailure = function() {
	throw new Error("JSExample API Failure call");
}

// API status (test to match NickServ)
JSExample.prototype.apiStatus = function() {
	return "JSExample API Status call";
}

// API callBack test
JSExample.prototype.apiCallback = function(text) {
	this._apiCBTest = text;
	this.mods.interval.callBack("api-test", 1000, 1);
}

// API to call another API
JSExample.prototype.apiCallAPI = function(plugin, api, param1, param2) {
	this.mods.security.getPluginNames("JSExample:apiCallAPI");
	this.mods.plugin.callAPI(plugin, api, [param1, param2]);
}

// Interval: api-test
JSExample.prototype._apiTestInterval = function(params, mods, irc) {
	irc.sendMessage(this._debugChannel, "API-test interval result: " + this._apiCBTest);
}

// Interval: init
JSExample.prototype._initInterval = function(param, mods, irc) {
	irc.sendMessage(this._debugChannel, "Interval! Woo!");
}


// Interval: Trac RSS
JSExample.prototype._tracSvnInterval = function(param, mods, irc) {
	try {
		/*var newItems = this._getTracRoadmapItems(5, this._tracRSSLastItem);
		
		if (this._tracRSSLastItem) {
			for (var i = 0; i < newItems.length; i++) {
				irc.sendMessage(this._tracRSSChannel, "Trac: " + newItems[i].title + " (" + newItems[i].link + ") - " + newItems[i].desc);
			}
		}
		if (newItems.length > 0) {
			this._tracRSSLastItem = newItems[newItems.length - 1].date;
		}*/
		
		// Roadmap changes.
		var roadmapData = "" + this._tracRoadmap.getContents();
		roadmapData = roadmapData.replace(/[\r\n]+/g, "");
		
		var firstRun = true;
		var roadmapChan = this._debugChannel;
		for (var i in this._tracRoadmapLastData) {
			firstRun = false;
			roadmapChan = this._tracRoadmapChannel;
			break;
		}
		
		var roadmapRegExp = new RegExp("class=\"milestone\".*?class=\"description\"", "gi");
		
		var item;
		while ((item = roadmapRegExp.exec(roadmapData))) {
			var itemObj = new Object();
			itemObj.name = /<em>(.*?)<\/em>/i.exec(item)[1];
			itemObj.percent = 1 * /class="percent">(.*?)\%</i.exec(item)[1];
			itemObj.ticketsOpen = 1 * /\?status=assigned[^"]+">(.*?)</i.exec(item)[1];
			itemObj.ticketsClosed = 1 * /\?status=closed[^"]+">(.*?)</i.exec(item)[1];
			itemObj.ticketsTotal = itemObj.ticketsOpen + itemObj.ticketsClosed;
			itemObj.all = [
					itemObj.name, itemObj.percent, itemObj.ticketsOpen, itemObj.ticketsClosed
				].join(",");
			
			var lastObj = null;
			if (this._tracRoadmapLastData && (itemObj.name in this._tracRoadmapLastData)) {
				lastObj = this._tracRoadmapLastData[itemObj.name];
			}
			
			//if (firstRun && !lastObj) {
			//	lastObj = {
			//			name: itemObj.name, percent: 100,
			//			ticketsOpen: 0, ticketsClosed: 0, ticketsTotal: 0,
			//			all: itemObj.name + ",0,0,0"
			//		};
			//}
			
			if (lastObj) {
				if ((lastObj.ticketsOpen > 0) && (itemObj.ticketsOpen == 0) && (itemObj.ticketsTotal > 0)) {
					irc.sendMessage(roadmapChan, "Trac: '" + itemObj.name + "' is now complete (all " +
							itemObj.ticketsTotal + " tickets closed)! :-D");
					
				} else if (itemObj.ticketsOpen < lastObj.ticketsOpen) {
					var count = (lastObj.ticketsOpen - itemObj.ticketsOpen);
					
					irc.sendMessage(roadmapChan, "Trac: '" + itemObj.name + "' has gone from " +
							lastObj.percent + "% complete to " +
							itemObj.percent + "% complete :-) (" +
							count + (count == 1 ? " ticket" : " tickets") + " closed, now " +
							itemObj.ticketsOpen + " of " +
							itemObj.ticketsTotal + (itemObj.ticketsTotal == 1 ? " ticket" : " tickets") + " remain).");
					
				} else if (itemObj.ticketsOpen > lastObj.ticketsOpen) {
					var count = (itemObj.ticketsOpen - lastObj.ticketsOpen);
					
					irc.sendMessage(roadmapChan, "Trac: '" + itemObj.name + "' has gone from " +
							lastObj.percent + "% complete to " +
							itemObj.percent + "% complete :-( (" +
							count + (count == 1 ? " ticket" : " tickets") + " opened, now " +
							itemObj.ticketsOpen + " of " +
							itemObj.ticketsTotal + (itemObj.ticketsTotal == 1 ? " ticket" : " tickets") + " remain).");
				}
				
			} else if (!firstRun) {
				irc.sendMessage(roadmapChan, "Trac: '" + itemObj.name + "' is " +
						itemObj.percent + "% complete (" +
						itemObj.ticketsOpen + " of " +
						itemObj.ticketsTotal + " tickets remain).");
			}
			this._tracRoadmapLastData[itemObj.name] = itemObj;
		}
		mods.interval.callBack(param, 30000 /* 30 seconds */, 2);
	} catch (ex) {
		irc.sendMessage(this._debugChannel, "_tracSvnInterval exception: line " + ex.lineNumber + ", " + ex);
		mods.interval.callBack(param, 300000 /* 5 minutes */, 2);
	}
}


// Command: JS
JSExample.prototype.commandJS = function(mes, mods, irc) {
	irc.sendContextReply(mes, "JS Rocks!");
}
// Note: This maps internally to the generic helpCommandJS.
//       It is purely convenience (and nice OO).
JSExample.prototype.commandJS.help = ["Says something about JS."];


// Command: Test
JSExample.prototype.commandTest = function(mes, mods, irc) {
	irc.sendContextReply(mes, "Our name: " + mods.security.getPluginName(0));
{'fraction_non_alphanumeric': 0.15371678439286915, 'fraction_numerical': 0.008619239825092499, 'mean_word_length': 3.4792843691148776, 'pattern_counts': {'":': 0, '<': 47, '<?xml version=': 0, '>': 43, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '877578', 'n_tokens_mistral': 9093, 'n_tokens_neox': 8030, 'n_words': 1939}