FB Wall Manager

Manages Wall Posts for Various FB Games

// ==UserScript==
// @name           FB Wall Manager
// @namespace      MerricksdadWallManager
// @description    Manages Wall Posts for Various FB Games
// @include        http*://www.facebook.com/pages/FB-Wall-Manager/*
// @license        http://creativecommons.org/licenses/by-nc-nd/3.0/us/
// @grant          GM_setValue
// @grant          GM_getValue
// @grant          GM_xmlhttpRequest
// @grant          GM_registerMenuCommand
// @grant          GM_addStyle
// @grant          GM_log
// @grant          GM_openInTab
// @grant          GM_getResourceURL
// @version        3.1.10
// @copyright      Charlie Ewing except where noted
// @require https://greasyfork.org/scripts/416-wm-common-library/code/WM%20Common%20Library.user.js
// @require https://greasyfork.org/scripts/417-wm-debug-console/code/WM%20Debug%20Console.user.js
// @require https://greasyfork.org/scripts/418-js-forms-library-b/code/JS%20Forms%20Library%20B.user.js
// @require https://greasyfork.org/scripts/419-wm-config-interface/code/WM%20Config%20Interface.user.js
// @require https://greasyfork.org/scripts/420-wm-graph-api-interface-beta-branch/code/WM%20Graph%20API%20Interface%20(Beta%20Branch).user.js
// @resource       IconSheet http://i.imgur.com/sLxzUA6.png
// ==/UserScript==

// retired libraries
// @resource       IconSheet http://images.wikia.com/fbwm/images/c/c0/Images.png
// @require        http://userscripts.org/scripts/source/29910.user.js
// @require        http://userscripts.org/scripts/source/129006.user.js
// @resource       IconSheet http://i1181.photobucket.com/albums/x430/merricksdad/images.png
// @require        http://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js
// http://ajax.googleapis.com/ajax/libs/jquery/1.2.6/jquery.js

//userscripts library locations
// @require        http://userscripts.org/scripts/source/123889.user.js
// @require        http://userscripts.org/scripts/source/128747.user.js
// @require        http://userscripts.org/scripts/source/150983.user.js
// @require        http://userscripts.org/scripts/source/152610.user.js
// @require        http://userscripts.org/scripts/source/150032.user.js

// for testing only
// @include        file:///C:/FB-Wall-Manager/*

// retired autolike functions
// @include        /^https?:\/\/www\.facebook\.com\/.*\/posts\/.*/

// Based on script built by Joe Simmons in Farmville Wall Manager


(function() {
//***************************************************************************************************************************************
//***** Preload
//***************************************************************************************************************************************
	//dont run in iframes
	try {
		//this does not mean we are using GM's unsafe window
		var unsafeWindow = unsafeWindow || window.wrappedJSObject || window;
		if (unsafeWindow.frameElement != null) return;
	} catch(e) {log("preload: "+e);}

//***************************************************************************************************************************************
//***** Debug Object
//***************************************************************************************************************************************
	if (debug) {
		debug.init();
		if (debug.initialized) log("Debug Console Initialized");
	}

//***************************************************************************************************************************************
//***** Globals
//***************************************************************************************************************************************
	this.WallManager={	
		paused : false,
		fetchPaused : false,
		requestsOpen : 0,
		reqTO : 30000,
		newSidekicks : [],

		accDefaultText : "Got this!",
		failText : "Oh no! Sorry pardner!",
		overLimitText : "Limit reached!",

		version:"3.1.10",
		currentUser:{
			id:"",
			profile:"",
			alias:""
		},
		resources:{
			iconsURL:GM_getResourceURL("IconSheet")
		},
		apps:{},
		posts:{},
		history:{},
		config:null,
		opts:{},
		quickOpts:{},
		displayGroups:{},
		likeQueue:[],
		switches:{
			manualAuthToken:true
		},

		statusText : {
			"20":"Sidekick returned force accept",
			"3":"Marked as accepted by user",
			"2":"Responseless Collection",
			"1":"Accepted",
			"0":"Unknown",
			"-1":"Failed",
			"-2":"None Left",
			"-3":"Over Limit (App)",
			"-4":"Over Limit, Sent One Anyway",
			"-5":"Server Error",
			"-6":"Already Got",
			"-7":"Server Down For Repairs",
			"-8":"Problem Getting Passback Link",
			"-9":"Final Request Returned Null Page",
			"-10":"Final Request Failure",
			"-11":"Expired",
			"-12":"Not a Neighbor",
			"-13":"Requirements not met",
			"-14":"Timeout",
			"-15":"Unrecognized Response",
			"-16":"Passback Link is missing",
			"-17":"Window Missing",
			"-18":"Marked as failed by user",
			"-20":"Sidekick returned force fail",
			
			"-19":"Over Limit (Bonus Type)",
			"-21":"Cancelled mid-process by user",
			
		},
		
		sortGroups : function(params){
			params=params||{};
			params.direction=(WM.quickOpts.groupDirection=(params.direction||WM.quickOpts.groupDirection||"desc")); //default descending to keep time ordered posts in order newest to oldest
			WM.saveQuickOpts();			
			
			//reorder the groups
			var groupsArray=[];
			for (var g in WM.displayGroups) {
				groupsArray.push({id:g,node:WM.displayGroups[g].parentNode,box:WM.displayGroups[g]});
			}
			
			if (["asc","ascending"].inArray(params.direction.toLowerCase())) groupsArray.sort(function(a,b){return a.id>b.id;});
			else if (["desc","descending"].inArray(params.direction.toLowerCase())) groupsArray.sort(function(a,b){return a.id<b.id;});
			
			WM.displayGroups={};
			for (var g=0; g<groupsArray.length; g++) {
				WM.displayGroups[groupsArray[g].id]=groupsArray[g].box;
				WM.console.feedNode.appendChild(groupsArray[g].node);
			}
		},

		newGroup : function(params){
			params=params||{};

			//prevent duplicates
			if (WM.displayGroups[params.by]||null) return WM.displayGroups[params.by];
			
			//create the nodes
			var box;
			var group=createElement("div",{className:"listItem"},[
				createElement("div",{className:"line", onclick:function(){
					//toggle rollout
					with (this.nextSibling) className=className.swapWordB((className.containsWord("collapsed")),"expanded","collapsed");
					with (this.firstChild.firstChild) className=className.swapWordB((className.containsWord("treeCollapse"+WM.opts.littleButtonSize)),"treeExpand"+WM.opts.littleButtonSize,"treeCollapse"+WM.opts.littleButtonSize);
				}},[
					createElement("div",{className:"littleButton",title:"Toggle Content"},[
						createElement("img",{className:"resourceIcon treeCollapse"+WM.opts.littleButtonSize}),
					]),
					createElement("label",{textContent:params.label||params.by})
				]),
				box=createElement("div",{className:"subsection rollout expanded"}),
			]);
						
			//add it to our group list
			WM.displayGroups[params.by]=box;
			
			WM.sortGroups();
			
			return box;
		},
		
		pauseCollecting : function(doPause){
			var isPaused;
			if (exists(doPause)) isPaused = (WM.paused = doPause);
			else isPaused=(WM.paused = !WM.paused);
			var btn=WM.console.pauseCollectButton;
			btn.className = btn.className.swapWordB(isPaused,"oddGreen","oddOrange");
			btn.title = (isPaused)?"Start Automatic Collection":"Pause Automatic Collection";
			var img = btn.childNodes[0];
			img.className = img.className.swapWordB(isPaused,"playRight24","stop24");
		},

		pauseFetching : function(doPause){
			var isPaused;
			if (exists(doPause)) isPaused = (WM.fetchPaused = doPause);
			else isPaused=(WM.fetchPaused = !WM.fetchPaused);
			var btn=WM.console.pauseFetchButton;
			btn.className = btn.className.swapWordB(isPaused,"oddGreen","oddOrange");
			btn.title = (isPaused)?"Start Automatic Fetching":"Pause Automatic Fetching";
		},

		clearGroups : function(params){
			//destroy previous groups
			for (var g in WM.displayGroups){
				remove(WM.displayGroups[g].parentNode); //kill the node
				delete WM.displayGroups[g]; //remove from list
			}
		},

		clearPosts : function(){
			//remove all post nodes from the collector panel
			for (var p in WM.posts){
				if (WM.posts[p].node) remove(WM.posts[p].node);
			}
		},
		
		constructGroups : function(params){
			params=params||{};
			//this specifically allows a null so we can remove grouping
			var by=exists(params.by)?params.by:WM.quickOpts.groupBy;
			//if nothing changed, just cancel
			if (by==WM.quickOpts.groupBy) return;
			
			//set the new group order
			WM.quickOpts.groupBy=by;
			WM.saveQuickOpts();
			
			WM.clearGroups();
		},
		
		sortPosts : function(params){
			params=params||{};
			params.direction=(WM.quickOpts.sortDirection=(params.direction||WM.quickOpts.sortDirection||"desc")); //default descending to keep time ordered posts in order newest to oldest
			params.by=(WM.quickOpts.sortBy=(exists(params.by)?params.by:(WM.quickOpts.sortBy||"created_time"))); //default by date
			WM.saveQuickOpts();

			//convert to array
			var postsArray=methodsToArray(WM.posts);

			//sort
			postsArray.sort(function(a,b){
				if (["ascending","asc"].inArray(params.direction.toLowerCase())) return a[params.by]>b[params.by];
				if (["descending","desc"].inArray(params.direction.toLowerCase())) return a[params.by]<b[params.by];
			});

			//convert back to object
			WM.posts=arrayToMethods(postsArray);
		},

		doWhichTestTree : function(post, testList, testData, custom) {try{
			//match post to an app
			var app=post.app;
			var synApp=app.synApp, w=null;

			for (var i=0,test;((test=testList[i]) && (w===null));i++) {

				//run only for tests that are not specifically disabled
				if (test.enabled===false) continue;
				
				//set find mode
				var findMode="auto";
			
				//finish constructing dynamic collection tests
				var ret = test.ret;
				if (custom) {
					if (!ret) ret = "dynamic"; //default to dynamic
					if (ret!="dynamic" && ret!="none" && ret!="exclude" && !ret.startsWith(synApp.appID)) ret=synApp.appID+ret; //add appID except to magic words
					findMode=test.findMode;
				}

			    //part to make dynamic collection tests work only if they are the correct appID
			    //also do not process disabled tests			    
				if (!custom || (custom && (!test.appID || (app.appID==test.appID)))){

					//if the test is not disabled (by test enabled both existing and being false)
					//OR if the test IS a dynamic test and the appID matches
					//OR if the test IS a dynamic test and no appID was supplied
					//then run the test

					//detect test type
					var testType=(test.search||null);
					var types=WM.grabber.methods;
					if (!testType) for (var tt=0,len=types.length; tt<len; tt++) {if (test[types[tt]]||"") {testType=types[tt];break;}}

					//select the type of data to use
					var src="";
					if (isArray(testType)){ //new search array format
						for (var t=0,tlen=testType.length;t<tlen;t++) src+=(testData[testType[t]]||"");
					}
					else src = (testData[testType]||""); //old test method like testType:text

					if (src){
						//begin processing this test
						var subTests=test.subTests, kids=test.kids, allowNone=false, subNumRange=test.subNumRange,text=(test.find||test[testType]||"");

						//process subtests array
						if (subTests && (findMode=="auto" || findMode=="subtests") && text) {
							for (var i2=0,subTest,found=false;((subTest=subTests[i2]) && (!found));i2++) {
								var testX = text.replace('{%1}',subTest).toLowerCase();

								//do a standard test with the replaced search string
								found=src.find(testX);

								//return a found value, replacing %1 with a lowercase no-space text equal to the subtest string
								w=(found)?ret.replace('{%1}',subTest.noSpaces().toLowerCase()):w;

								testX=null;
							}

						//process number array
						} else if (subNumRange && (findMode=="auto" || findMode=="subnumrange") && text){
							var start=parseInt(subNumRange.split(",")[0]), end=parseInt(subNumRange.split(",")[1]);
							for (var i2=start,found=false;((!found) && i2<=end);i2++)  {
								var testX = text.replace('{%1}',i2).toLowerCase();

								//do a standard test with the replaced search string
								found=src.find(testX);

								//return a found value, replacing %1 with a lowercase no-space text equal to the subtest string
								w=(found)?ret.replace('{%1}',i2):w;

								testX=null;
							}

						//process text array, process similar to subtests
						} else if (text && (findMode=="auto" || findMode=="basic") && (isArray(text))) {
							for (var i2=0,subTest,found=false;((subTest=text[i2]) && (!found));i2++) {
								var testX = subTest.toLowerCase();

								//do a standard test with the replaced search string
								found=src.find(testX);

								//return the same value no matter which element from the array is found
								w=(found)?ret:w;

								testX=null;
							}


						//process regex
						} else if (text && (test.regex||test.isRegex||null) ) {
							var mods = (test.mods||"gi");
							var testRegex = new RegExp(text,mods);
							var match=src.match(testRegex);
							if (match) match=match[0]; //always take the first match
							w=ret||match||w;



						//process single text
						} else if (text) {
							try{
								w=(src.find(text.toLowerCase() ))?ret:w;
							} catch(e){
								log("WM.doWhichTestTree:"+e);
								log("--app:"+app.appID);
								log("--test:"+JSON.stringify(test));
							}
						}

					}

					//see if test has type 2 subtests (child node tests based on parent test)
					w = ((kids && w)?WM.doWhichTestTree(post, kids, testData, custom):w) || w; //if kids return null, default to key found above

					//if this test tree returned "none", start over with next tree by replacing "none" with null
					//true "none" is handled in the which() function below
					if (w==="none") w=null;


				}//end custom checker
			}

			return w;
		}catch(e){log("WM.doWhichTestTree: "+e);}},

		which : function(post,params) {try{
			//prevent the rules manager from mistaking main as a post object
			if (!post) return;
			
			params=params||{};
			//match post to an app
			var w, app=post.app, synApp=app.synApp;

			//create various data for the tests to use
			if (!params.reid) post.testData = {
				title: (post.name||"undefined").toLowerCase(),
				msg: (post.message||"undefined").toLowerCase(),
				caption: (post.caption||"undefined").toLowerCase(),
				desc: (post.description||"undefined").toLowerCase(),
				link: (post.linkText||"undefined").toLowerCase(),
				url: Url.decode(post.linkHref).toLowerCase(),
				img: (post.picture||"undefined").toLowerCase(),
				fromName: post.fromName.toLowerCase(),
				fromID: post.fromID.toLowerCase(),
				targetName: "undefined", //","+post.getTargets("name").join(",").toLowerCase(),
				//targetID: "undefined", //","+post.getTargets("id").join(",").toLowerCase(),
				canvas: "undefined", //app.namespace.toLowerCase(),
				likeName: "undefined", //","+post.getLikes("name").join(",").toLowerCase(),
				likeID: "undefined", //","+post.getLikes("id").join(",").toLowerCase(),
				comments: "undefined", //post.getComments("message").join(" \n").toLowerCase(),
				commentorName: "undefined", //","+post.getComments("name").join(",").toLowerCase(),
				commentorID: "undefined", //","+post.getComments("id").join(",").toLowerCase(),
			};
			var testData=post.testData;

			//replacement for old options like body, either and html
			testData.body = testData.title+testData.caption+testData.desc;
			testData.either = testData.link+testData.body;
			testData.html = testData.fromID + testData.fromName + testData.targetID + testData.targetName + testData.message
				+ testData.href + testData.either + testData.img + testData.canvas + testData.likeID + testData.likeName
				+ testData.commentorID + testData.commentorName + testData.comments;

			var dynamicTests = WM.grabber.tests;

			//check user built dynamic tests first if enabled and told to run first
			if (WM.opts["dynamic"+app.appID] && WM.opts.dynamicFirst && dynamicTests) {
				w=WM.doWhichTestTree(post,dynamicTests, testData, true)||"none";
			}

			//process this game's tests if dynamic didn't already get one
			if (w=="none" || !w || w=="") {
				w=((tests=synApp.tests)?WM.doWhichTestTree(post,tests, testData):"none")||"none";
			}

			//check user built dynamic tests last if enabled and not told to run first
			if (w=="none" || !w || w=="") {
				if (WM.opts["dynamic"+app.appID] && !WM.opts.dynamicFirst && dynamicTests) {
					w=WM.doWhichTestTree(post,dynamicTests,testData, true)||"none";
				}
			}

			//switch to undefined collection if enabled
			w=(w==="none" && app.opts["doUnknown"])?"doUnknown":w;

			return w;
		}catch(e){log("WM.which: "+e);}},

		resetAccepted : function(params) {
			params=params||{};
			var ask=WM.opts.historyConfirmClear;
			if (params.noConfirm || !ask || (ask && confirm("Delete all history for this profile?"))){
				doAction(function(){
					WM.history={};
					setOpt('history_'+WM.currentUser.profile,'{}');
				});
			}
		},

		onWindowResize : function(){
			WM.resizeConsole();
		},
		
		onHeartbeat : function(){
			if (WM.rulesManager.enabled) {

				//affect rules at the base level
				WM.rulesManager.doEvent("onHeartbeat",{});
				
				//affect rules at the app level
				if (WM.opts.heartbeatAffectsApps) {
					for (var a in WM.apps) {
						(function(){
							WM.rulesManager.doEvent("onHeartbeat",WM.apps[a]);
						})();
					}
				}

				//affect rules at the post level
				if (WM.opts.heartbeatAffectsPosts) {
					for (var p in WM.posts) if (!WM.posts[p].isGhost) {
						(function(){
							WM.rulesManager.doEvent("onHeartbeat",WM.posts[p]);
						})();
					}
				}

				//affect rules at the rule level
				if (WM.opts.heartbeatAffectsRules) {
					for (var r=0; r<WM.rulesManager.rules.length; r++) {
						(function(){
							WM.rulesManager.doEvent("onHeartbeat",WM.rulesManager.rules[r]);
						})();
					}
				}

				//affect rules at the feed and feed filter levels
				if (WM.opts.heartbeatAffectsFeeds || WM.opts.heartbeatAffectsFeedFilters) {
					var feeds=WM.feedManager.feeds;
					for (var f=0,len=feeds.length; f<len; f++) {
						//do the feed
						if (WM.opts.heartbeatAffectsFeeds) (function(){
							WM.rulesManager.doEvent("onHeartbeat",feeds[f]);
						})();
						
						//do the feed filters
						if (WM.opts.heartbeatAffectsFeedFilters) {
							for (var ff in feeds[f].filters){
								(function(){
									WM.rulesManager.doEvent("onHeartbeat",feeds[f].filters[ff]);
								})();
							}
						}
					}
				}
			}
			
			//check for new sidekick arrivals
			if (isArrayAndNotEmpty(WM.newSidekicks)) {
				while (WM.newSidekicks.length>0) {
					var app=WM.newSidekicks.shift();
					app.fetchPosts();
				}
			}
			
			//check for autolike queue contents
			var quePost = WM.checkAutoLikeQue();
			if (quePost) {
				//log([quePost.fn,quePost.post.id]);
				switch (quePost.fn) {
					case "like":quePost.post.like();break;
					case "comment":quePost.post.comment(quePost.say);break;
				}
			}
			
		},

		//this is for when the WM.config and globalConfig settings change
		onSave : function() {
			//recopy the settings array from WM.config
			WM.updateSettingsValues();

			//hide or show counters
			if (WM.opts.showcounters) WM.showCounters(); else WM.hideCounters();

			//update intervals
			WM.setIntervals();

			//set new user colors
			WM.setColors();
			
			//update config settings
			WM.changeConfigSettings();

			//update those settings we use as global variables
			WM.changeDebugSettings();
			
			//set console heights
			//WM.resizeConsole();
		},

		updateSettingsValues : function(){try{
			WM.opts = WM.config.values;
			
			//new: do this for each of the apps too
			for (var a in WM.apps) WM.apps[a].opts=WM.apps[a].config.values;
			
		}catch(e){"WM.updateSettingsValues: "+e}},

		getAccText: function(appID,w,past,status){
			var app=WM.apps[appID].synApp;
			//detect and use a status code message
			if (!(status>-1 || status==-4 || status==-6)) return WM.statusText[status];
			//or return a generic message based on post type
			else return (w=="dynamic")?"Dynamic Grab"+((past)?"bed":""):(((w.find("send")?"Sen"+((past)?"t":"d")+" ":w.find("wishlist")?"":"G"+((past?"o":"e"))+"t ") + (app.userDefinedTypes[w]||app.accText[w])) || ((past)?WM.accDefaultText:"Get Unknown") || ((w.startsWith(app.appID+"doUnknown"))?"Unknown":"") );
		},

		stopCollectionOf : function(w){
			for (var p in WM.posts) if (!WM.posts[p].isGhost && WM.posts[p].which==w) WM.posts[p].stopCollect();
		},

		startCollectionOf : function(w){
			for (var p in WM.posts) if (!WM.posts[p].isGhost && WM.posts[p].which==w) WM.posts[p].collect();
		},

		pauseByType : function(app,w){
			if (!isArray(w)) w=[w];
			//mark as paused all those posts not yet done
			for (var p in WM.posts) if (!WM.posts[p].isGhost && w.inArray(WM.posts[p].which)) WM.posts[p].pause();
			//store the paused type but dont save it
			var a=(app.parent||app);
			for (var i=0; i<w.length; i++) {
				var t=w[i];
				//add it to the array without making a duplicate
				if (!a.typesPaused.inArray(t)) {
					a.typesPaused.push(t);
					//add a visible node
					a.typesPausedNode.appendChild(
						a.pausedTypesListNodes[t]=createElement("div",{className:"line"},[
							createElement("span",{textContent:(a.userDefinedTypes[t]||a.accText[t])+" ("+t+") "}),
							createElement("div",{className:"littleButton oddGreen", title:"Unpause Type"},[
								createElement("img",{className:"resourceIcon playRight"+WM.opts.littleButtonSize,onclick:function(){
									WM.unPauseByType(a,t);
								}})
							])
						])
					);
				}
			}
		},

		unPauseByType : function(app,w){
			if (!isArray(w)) w=[w];
			//unpause all those posts not yet done
			for (var p in WM.posts) if (!WM.posts[p].isGhost && w.inArray(WM.posts[p].which)) WM.posts[p].unPause();
			//remove paused type from list but dont save it
			var a=(app.parent||app);
			for (var i=0; i<w.length; i++) {
				//remove the visible node
				remove (a.pausedTypesListNodes[w[i]]);
				//delete the visible node entry
				delete a.pausedTypesListNodes[w[i]];
				//remove it from the array
				a.typesPaused.removeByValue(w[i]);
			}
		},

		setAsAccepted : function(comment,status, post) {try{
			var app=post.app;
			var synApp=app.synApp;
			post.state="accepted";
			post.status=status;
			post.accept();

			WM.history[post.id]={status:status, date:timeStamp(), which:(post.which||"undefined").removePrefix(synApp.appID), appID:app.appID};
			setOptJSON('history_'+WM.currentUser.profile,WM.history);

			//do friend tracking
			if (WM.opts.useFriendTracker && WM.opts.trackAccepted){
				WM.friendTracker.trackStatus(post,true);
			}

			var postNode=post.node||$("post_"+post.id);
			if (postNode){
				var link=selectSingleNode(".//a[contains(@class,'linkText')]",{node:postNode});

				var text=WM.getAccText(synApp.appID,post.which,true,status);

				link.textContent = (comment || text || WM.statusText[status] || WM.accDefaultText);
				WM.updatePostStatus(post.id);
			}

			app.acceptCount++; 

			//perform the onAccepted event
			WM.rulesManager.doEvent("onAccepted",post);

			//try autolike
			try{
				if (WM.opts.useautolike && (WM.opts.autolikeall || WM.opts.autolikeaccepted || (WM.opts.autolikesent && (post.which||"undefined").startsWith("send")) )) {
					if (!WM.opts["nolike"+app.appID]){
						WM.queAutoLike(post);
						//post.like();
					}
				}
			} catch(e){log("setAsAccepted: autolike failed: "+e,{level:3});}
			//try autocomment
			
			try{
				if (WM.opts.useautocomment && (WM.opts.autolikeall || WM.opts.autolikeaccepted || (WM.opts.autolikesent && (post.which||"undefined").startsWith(synApp.appID+"send")) )) {
					if (!WM.opts["nolike"+app.appID]){
						//setTimeout(function(){post.comment();},100+(WM.opts.autolikedelay*1000));
						WM.queAutoComment(post,null);
					}
				}
			} catch(e){log("setAsAccepted: autocomment failed: "+e,{level:3});}
			
		}catch(e){log("WM.setAsAccepted: "+e);}},

		disableOpt : function(w,app){try{
			var targetConfig=(app||null)?app.config:WM.config;
			((app||null)?app.opts:WM.opts)[w]=false;
			targetConfig.set(w,false);
			targetConfig.save();
			debug.print([w,app,false]);
		}catch(e){log("WM.disableOpt: "+e);}},

		enableOpt : function(w,app){try{
			var targetConfig=(app||null)?app.config:WM.config;
			((app||null)?app.opts:WM.opts)[w]=true;
			targetConfig.set(w,true);
			targetConfig.save();
			debug.print([w,app,true]);
		}catch(e){log("WM.enableOpt: "+e);}},

		setOpt : function(w,v,app){try{
			var targetConfig=(app||null)?app.config:WM.config;
			((app||null)?app.opts:WM.opts)[w]=v;
			targetConfig.set(w,v);
			targetConfig.save();
			debug.print([w,app,v]);
		}catch(e){log("WM.setOpt: "+e);}},

		resetCounters : function(){try{
			for (var a in WM.apps) WM.apps[a].resetCounter();
		}catch(e){log("WM.resetCounters: "+e);}},

		setAsFailed : function(comment, status, post){try{
			var app=post.app;
			var synApp=app.synApp;
			var postNode=post.node||$("post_"+post.id);

			//special effects for timeout and cancelProcess
			if ((!WM.opts.failontimeout && status==-14) || status==-21) {
				post.state="timeout";
				post.timeout();
				if (status==-14) WM.rulesManager.doEvent("onTimeout",post);

			} else {
				post.state="failed";
				post.fail(); // don't pass true or it will loop here

				WM.history[post.id]={status:status, date:timeStamp(), which:(post.which||"undefined").removePrefix(synApp.appID), appID:app.appID};
				setOptJSON('history_'+WM.currentUser.profile,WM.history);
				
				WM.rulesManager.doEvent("onFailed",post);
			}
			post.status=status;

			//do friend tracking
			if (WM.opts.useFriendTracker && WM.opts.trackFailed){
				WM.friendTracker.trackStatus(post,false);
			}
			
			if (postNode) {
				var link=selectSingleNode(".//a[contains(@class,'linkText')]",{node:postNode});
				if (link) {
					//I can see no reason the link should be missing, but since its been proven to fail, here is a patch
					link.textContent = (comment || WM.statusText[status] || WM.failText);
				}
				WM.updatePostStatus(post.id);
			}

			app.failCount++; 
			
			//try autolike
			try{
				if (WM.opts.useautolike && WM.opts.autolikeall) {
					if (!WM.opts["nolike"+app.appID]){
						WM.queAutoLike(post);
						//post.like();
						//setTimeout(function(){post.like();},100+(WM.opts.autolikedelay*1000));
					}
				}
			} catch(e){log("setAsFailed: autolike failed: "+e,{level:3});}
			//try autocomment
			
			try{
				if (WM.opts.useautocomment && WM.opts.autolikeall) {
					if (!WM.opts["nolike"+app.appID]){
						//setTimeout(function(){post.comment();},100+(WM.opts.autolikedelay*1000));
						WM.queAutoComment(post,null);
					}
				}
			} catch(e){log("setAsFailed: autocomment failed: "+e,{level:3});}
			
		}catch(e){log("WM.setAsFailed: "+e);}},

		setPriority : function(){
			var postNode=selectSingleNode(".//ancestor::*[starts-with(@id,'post_')]",{node:this});
			var id=postNode.id.replace("post_","");
			WM.posts[id]["priority"]=this.getAttribute("name");
			remove(postNode);
			WM.posts[id].draw();
		},

		clearURL : function(tab){
			WM.collector.close(tab);
			WM.requestsOpen--;
		},
		
		//constantly update sidekick channel data
		skChannel : {},
		
		fetchSidekickData : function(){try{
			if (WM) {
				var node=selectSingleNode("./div",{node:$("wmDataDump")});
				while (node){
					log("WM.fetchSidekickData: found "+JSON.parse(node.getAttribute("data-ft")));
					WM.skChannel=mergeJSON(WM.skChannel,JSON.parse(node.getAttribute("data-ft")));
					remove(node);
					node=selectSingleNode("./div",{node:$("wmDataDump")});
				}
				setTimeout(WM.fetchSidekickData,1000);
			}
		}catch(e){log("WM.fetchSidekickData: "+e);}},		
		
		//this is WM3's method of handling conversations with sidekicks
		onFrameLoad3 : function(tab){try{
			log("onFrameLoad3(): tab="+tab.id);
						
			var postID=tab.postID||tab.id;
			var post=tab.post||WM.posts[postID];

			//detect if post process was cancelled by user
			if (post.processCancelled){
				//reset the cancel memory
				post.processCancelled = false;
				log("onFrameLoad3: process cancelled by user");
				//set the timeout flag even though its not timed out
				WM.setAsFailed(null,-21,post);
				WM.clearURL(tab);
				return;
			}

			//detect if valid WM.collector window still exists
			var windowExists=(tab.hwnd && !tab.hwnd.closed);
			/*try{
				var testUrl=tab.hwnd.location.toString();
			} catch(e) {
				windowExists=false; 
			}*/
			
			//make sure the post object still exists
			if (!(post||null)){
				log("onFrameLoad3: post is null");
				WM.clearURL(tab);
				return;
			}

			//check if window object is missing
			if (!windowExists) {
				log("windowExists = false");
				if (!tab.hwnd) log("onFrameLoad3: tab.hwnd is null");
				if (tab.hwnd.closed) log("onFrameLoad3: tab.hwnd is closed");
				WM.setAsFailed(null,-17,post);
				WM.clearURL(tab);
				return;
			}
			
			//check timer on this open post
			var openTime=tab.openTime;
			var nowTime=timeStamp();
			if ((WM.opts.reqtimeout*1000)<(nowTime-openTime)){
				log("onFrameLoad3: post timed out");
				WM.setAsFailed(null,-14,post);
				WM.clearURL(tab);
				return;
			}
			
			//create the retry function
			var retry=function(){setTimeout(function(){WM.onFrameLoad3(tab); return;},1000); return;};
			
			//look for status data
			var tabID = tab.id;
			var skData = WM.skChannel[tabID]||null;
			if (skData) {
				//data exists for this post
				if (skData.status) {
					//status is available
					delete WM.skChannel[tabID];
					
					//get useful post data
					var app=post.app; var synApp=app.parent||app;
					var postNode=post.node||$("post_"+post.id);
					var link=selectSingleNode(".//a[contains(@class,'linkText')]",{node:postNode});
					var w=post.which||"undefined";
					
					//confirm status
					var gotItem=((skData.status>0) || (skData.status==-6) || (skData.status==-4) || (skData.status==-15 && WM.opts.accepton15));
					var failedItem=(skData.status<0);
					
					if (gotItem){
						//build debug block
						switch(skData.status){
							case -6: case -4: case 1:
								// this bonus is available or we still have the ability to send something for no return
								//dont break before next
							case -15: case 2:
								if (!synApp.flags.requiresTwo){
									WM.setAsAccepted(null, skData.status, post);
								}
								break;

							default:
								//should not have come here for any reason, but if we did assume its a status code I didnt script for
								WM.setAsFailed(null, skData.status, post);
								log("onFrameLoad3: unexpected status code: "+skData.status,{level:2});
								break;
						}
					} else {
						WM.setAsFailed(null,skData.status,post);
					}
					
					// click "yes" to accept it, if we got this far we actually found an accept button
					if(synApp.flags.requiresTwo && gotItem) {
						if (skData.nopopLink) {
							var req; req=GM_xmlhttpRequest({
								method: "GET",
								url: skData.nopopLink,
								timeout: WM.opts.reqtimeout*1000,
								onload: function(response) {
									//search for error messages
									var test=response.responseText;
									if (test==""){
										//no text was found at requested href
										log("onFrameLoad3: final stage: null response",{level:2});
										WM.setAsFailed(null, -9,post);
									} else {
										//if no errors then we got it
										WM.setAsAccepted(null, skData.status,post);
									}
									WM.clearURL(tab);
									if(req)req=null;
								},

								onerror: function(response) {
									log("onFrameLoad3: final stage: error returned",{level:2});
									//if final request fails, drop the request for now
									WM.setAsFailed(null, -10,post);
									WM.clearURL(tab);
									if(req)req=null;
								},

								onabort: function(response) {
									log("onFrameLoad3: final stage: request aborted",{level:2});
									WM.setAsFailed(null, -10,post);
									WM.clearURL(tab);
									if(req)req=null;
								},
								ontimeout: function(response) {
									log("onFrameLoad3: final stage: request timeout",{level:2});
									WM.setAsFailed(null, -10,post);
									WM.clearURL(tab);
									if(req)req=null;
								},
							});

						} else {
							log("onFrameLoad3: skData.nopopLink is null and a string was expected",{level:3});
							WM.setAsFailed(null, -16,post);
							WM.clearURL(tab);
							return;
						}
					} else WM.clearURL(tab); //<- default page clearer, do not remove
				} else retry();
			} else {
				retry();
				//send the tab its init message (again)
				tab.hwnd.postMessage({
					channel:"WallManager",
					msg:1,
					tabID:tab.id,
				},"*");
				//log("useGM_openInTab: "+WM.opts.useGM_openInTab);
			}
			
		}catch(e){log("WM.onFrameLoad3: "+e);}},		
		
		//this is WM1-2's method of handling conversation with sidekicks
		//WM3 defaults to this if sidekick is not WM3 compatible
		onFrameLoad : function(tab,noDebug){try{
			//tab object contains {id,post,url}
			if (!noDebug) log("onFrameLoad()",{level:0});

			var id=tab.id; var post=tab.post||WM.posts[id];
			if (!(post||null)) {
				//resource deleted while post was out
				WM.clearURL(tab);
				return;
			}

			//detect if post process was cancelled by user
			if (post.processCancelled){
				//reset the cancel memory
				post.processCancelled = false;
				log("onFrameLoad3: process cancelled by user");
				//set the timeout flag even though its not timed out
				WM.setAsFailed(null,-21,post);
				WM.clearURL(tab);
				return;
			}

			var app=post.app;
			var synApp=app.parent||app;

			var httpsTrouble=synApp.flags.httpsTrouble;
			var responseLess=synApp.flags.skipResponse;

			var postNode=post.node||$("post_"+post.id);
			var link=selectSingleNode(".//a[contains(@class,'linkText')]",{node:postNode});
			var w=post.which||"undefined";

			tab.tries=(tab.tries||0)+1;
			if (tab.tries>WM.opts.reqtimeout) {
				log("onFrameLoad: request timeout",{level:3});
				WM.setAsFailed(null, -14, post);
				WM.clearURL(tab);
				return;
			}

			var retry=function(){setTimeout(function(e){WM.onFrameLoad(tab, true);}, 1000);};

			var failedItem=false, gotItem=false, nopopLink;

			//check if window object is missing
			var windowExists=(tab.hwnd && !tab.hwnd.closed);
			if (!windowExists) {WM.setAsFailed(null,-17,post); WM.clearURL(tab); return;}
			//check if window document does not yet exist
			//if (!(tab.hwnd.document||null)) {retry(); return;}

			//get sidekick return value
			var hashMsg="",hashStatus=0;
			try{
				//if error encountered, reload the page
				if (tab.hwnd.document.title==="Problem loading page"){
					log("processPosts: problem loading page",{level:1});
					tab.hwnd.location.reload();
					retry();
					return;
				}

				var temphash = tab.hwnd.location.hash; //capture a hash if we can
				hashMsg = ((temphash)?temphash.removePrefix("#"):null) || tab.hwnd.location.href.split("#")[1];
				hashStatus=(responseLess)?2:(hashMsg||null)?parseInt(hashMsg.split('status=')[1].split("&")[0]):0;
				gotItem=((hashStatus>0) || (hashStatus==-6) || (hashStatus==-4) || (hashStatus==-15 && WM.opts.accepton15));
				failedItem=(hashStatus<0);
				if (!gotItem && !failedItem) {retry(); return;}

			} catch(e){
				var errText=""+e;
				if (errText.contains("hashMsg is undefined")) {
					//this known issue occurs when a page is not yet fully loaded and the
					//WM script tries to read the page content
					retry();
					return;
				}
				else if (errText.contains("Permission denied to access property")) {
					//we've reached some known cross domain issue
					if (responseLess) {
						//if the sidekick creator has chosen to use responseless collection
						//simply assume the page has loaded and mark the item as collected

						gotItem=true;failedItem=false;hashStatus=2;
					} else {
						console.log("WM.onFrameLoad(before retry): "+e);
						retry();
						return;
					}
				}
				else if (errText.contains("NS_ERROR_INVALID_POINTER")
					|| errText.contains("tab.hwnd.document is null") ) {

					WM.setAsFailed(null,-17,post);
					WM.clearURL(tab);
					return;
				}
				else {
					log("onFrameLoad: "+e,{level:3});
					retry();
					return;
				}
			}

			//if gotItem then we have been offered the item so far
			if (gotItem){
				//build debug block
		    		switch(hashStatus){
					case -6: case -4: case 1:
						// this bonus is available or we still have the ability to send something for no return
						if (synApp.flags.requiresTwo){
							try{
								nopopLink=hashMsg.split("&link=[")[1].split("]")[0];
							}catch(e){
								//known rare issue where no link is passed back by pioneer trail
							}
						}
						//dont break before next
					case -15: case 2:
						if (!synApp.flags.requiresTwo){
							WM.setAsAccepted(null, hashStatus,post);
						}
						break;

					default:
						//should not have come here for any reason, but if we did assume its a status code I didnt script for
						WM.setAsFailed(null, hashStatus,post);
						log("onFrameLoad: unexpected status code: "+hashStatus,{level:2});
						break;
		    		}
			} else {
				WM.setAsFailed(null,hashStatus,post);
			}


			// click "yes" to accept it, if we got this far we actually found an accept button
			if(synApp.flags.requiresTwo && gotItem) {
				if (nopopLink) {
					var req; req=GM_xmlhttpRequest({
						method: "GET",
						url: nopopLink,
						timeout: WM.opts.reqtimeout*1000,
						onload: function(response) {
							//search for error messages
							var test=response.responseText;
							if (test==""){
								//no text was found at requested href
								log("onFrameLoad: final stage: null response",{level:2});
								WM.setAsFailed(null, -9,post);
							} else {
								//if no errors then we got it
								WM.setAsAccepted(null, hashStatus,post);
							}
							WM.clearURL(tab);
							if(req)req=null;
						},

						onerror: function(response) {
							log("onFrameLoad: final stage: error returned",{level:2});
							//if final request fails, drop the request for now
							WM.setAsFailed(null, -10,post);
							WM.clearURL(tab);
							if(req)req=null;
						},

						onabort: function(response) {
							log("onFrameLoad: final stage: request aborted",{level:2});
							WM.setAsFailed(null, -10,post);
							WM.clearURL(tab);
							if(req)req=null;
						},
						ontimeout: function(response) {
							log("onFrameLoad: final stage: request timeout",{level:2});
							WM.setAsFailed(null, -10,post);
							WM.clearURL(tab);
							if(req)req=null;
						},
					});

				} else {
					log("onFrameLoad: nopopLink is null and a string was expected",{level:3});
					WM.setAsFailed(null, -16,post);
					WM.clearURL(tab);
					return;
				}
			} else WM.clearURL(tab);

		}catch(e){log("WM.onFrameLoad: "+e);}},

		toggle : function(opt,app){
			var targetConfig=(app||null)?app.config:WM.config;
			var targetOpts=(app||null)?app.opts:WM.opts;
			if (targetOpts[opt]){
				targetConfig.set(opt, false);
				targetOpts[opt] = false;
			} else {
				targetConfig.set(opt, true);
				targetOpts[opt] = true;
			}
               targetConfig.save();
		},

		getAppDropDownList : function(selectedIndex,allowBlank){
			var retApps=[];
			//add the fake initial option
			retApps.push(createElement("option",{textContent:"select an app",value:""}));
			retApps.push(createElement("option",{textContent:"* All",value:""}));
			if (allowBlank) retApps.push(createElement("option",{textContent:"all apps",value:""}));

			for(var i in WM.apps){
				if (!WM.apps[i].parent) {
					var elem = createElement("option",{textContent:WM.apps[i].name,value:i});
					if ((selectedIndex||null) == i) elem.selected = true;
					retApps.push(elem);
				}
			}
			return retApps;
		},

		getBonusDropDownList : function(params){
			params=params||{};
			var selected = params.selected||"";
			var appID = params.appID||null;
			var dropID = params.dropID||false; //force the element value to drop its appID prefix

			var optsret=[], bonuses={};
			if (appID) bonuses = mergeJSON(WM.apps[appID].accText,WM.apps[appID].userDefinedTypes);
			bonuses["dynamic"]="* Dynamic: Just Grab It";
			bonuses["none"]="* None: Break Identification Circuit";
			bonuses["wishlist"]="* Flag as Wishlist";
			bonuses["exclude"]="* Exclude: Prevent Collection";
			bonuses["send"]="* Send Unknown";
			bonuses["doUnknown"]="* Get Unknown";

			//create option values and names;
			for (var i in bonuses) {
				var elem
				if (appID) elem = createElement("option",{textContent:((i.startsWith(appID+"send"))?"Send ":((bonuses[i].substring(0,1)=="*")?"":"Get "))+bonuses[i],value:((dropID)?i.removePrefix(appID):i)});
				else elem = createElement("option",{textContent:bonuses[i],value:i});

				if (appID) {if (selected==((dropID)?i.removePrefix(appID):i) ) elem.selected = true;}
				else {if (selected==i) elem.selected=true;}

				optsret.push(elem);
			}

			return optsret;
		},

		reIDAll : function(){
			for (var p in WM.posts) {
				if (!WM.posts[p].isGhost && WM.posts[p].identify({reid:true}))
					WM.rulesManager.doEvent("onIdentify",WM.posts[p]);
			}
			WM.sortPosts(); //in this case sorting may cancel movetotop and movetobottom
			WM.clearGroups();
			WM.redrawPosts({postRedraw:true});
		},

		updatePostStatus : function(id){
			var status = WM.posts[id].status;
			var statusNode = selectSingleNode(".//*[contains(@class,'status')]",{node:$("post_"+id)});
			if (statusNode) statusNode.textContent="Status: "+(status||"0") + " " + (WM.statusText[status||"0"]);
			status=null; statusNode=null;
		},

		onLikePost : function(post){
			post.isLiked=true;
			return;
			
			//pre beta 40 stuff
			var postID=tab.id;
			var post=tab.post||WM.posts[postID];

			//detect if post process was cancelled by user
			if (post.processCancelled){
				//reset the cancel memory
				post.processCancelled = false;
				log("onLikePost: feedback cancelled by user");
				WM.collector.close(tab);
				return;
			}

			//detect if valid WM.collector window still exists
			var windowExists=(tab.hwnd && !tab.hwnd.closed);

			//check if window object is missing
			if (!windowExists) {
				log("onLikePost: tab.hwnd is null or closed");
				WM.collector.close(tab);
				return;
			}
			
			try{
				var like=tab.hwnd.location.hash.removePrefix("#").getUrlParam("status")=="1";
				if (like) {
					if (tab.post) {
						//tell the post it is liked
						tab.post.isLiked = true;
						//delete the post reference from the tab
						delete tab.post;
					}
					WM.collector.close(tab);
					return;
				}
			} catch (e){
				//log(""+e);
			}

			tab.tries=(tab.tries||0)+1;
			if (tab.tries<WM.opts.autoliketimeout) setTimeout(function(){WM.onLikePost(tab);}, 1000);
			else {
				log("onLikePost: unable to finish feedback",{level:3});
				doAction(function(){WM.collector.close(tab);});
			}
		},

		toggleSidekick : function(){
			var appID = this.id.split("master_")[1];
			var opt = !(WM.quickOpts["masterSwitch"][appID]||false); //toggle
			WM.quickOpts["masterSwitch"][appID]=opt;
			var className = this.parentNode.className;
			this.parentNode.className = ((opt)?className.removeWord("disabled"):className.addWord("disabled"));
			this.textContent=((opt)?"Disable":"Enable");
			WM.saveQuickOpts();
		},

		saveQuickOpts : function(){
			setOptJSON('quickopts_'+WM.currentUser.profile, WM.quickOpts);
		},

		setAppFilter : function(tab){
			WM.quickOpts.filterApp=tab.appFilter;
			WM.saveQuickOpts();
			WM.clearGroups();
			WM.redrawPosts({postRedraw:false,reorder:true});
			WM.rulesManager.doEvent("onSetAppFilter",WM.apps[tab.appFilter]);
			//debug.print(["Collection Tab Selected",WM.currentAppTab,WM.apps[tab.appFilter]]);
		},
		
		setDisplay : function(){
			var x=this.getAttribute("name");
			WM.quickOpts.displayMode=x;
			WM.saveQuickOpts();
			WM.redrawPosts({postRedraw:true,reorder:true});
			WM.setDisplayCols();
		},
		
		setDisplayCols : function(params){
			params=params||{};
			params.cols=params.cols||WM.quickOpts.displayCols;
			WM.quickOpts.displayCols=params.cols||1;
			WM.saveQuickOpts();
			with (WM.console.feedNode) {
				className=className
					.toggleWordB(params.cols==1,"singleCol")
					.toggleWordB(params.cols==2,"twoCol")
					.toggleWordB(params.cols==3,"threeCol")
					.toggleWordB(params.cols==4,"fourCol");
			}
		},

		redrawPosts : function(params){
			params=params||{};
			var feedNode=WM.console.feedNode;
			
			//set the proper display mode
			feedNode.className=feedNode.className
				.toggleWordB((WM.quickOpts.displayMode=="1" || WM.quickOpts.displayMode=="3"),"short");
			
			//avoid order issues by removing the posts from the panel
			WM.clearPosts();

			//redraw||reorder
			for (var p in WM.posts) {
				var post=WM.posts[p];
				if (!post.isGhost) {
					post.draw(params.postRedraw,params.reorder);
				}
			}
		},

		moveFloater : function(ev){
			if (isChrome) return;
			var img=this, offset=trueOffset(img), scrolled=trueScrollOffset(img),
				post=selectSingleNode(".//ancestor::div[starts-with(@id,'post')]",{node:img}),
				floater=$(post.id.replace("post","floater")), special={};

			//log( (scrolled.left) +","+ (scrolled.top) );

			special.x=(ev.clientX > (document.documentElement.clientWidth/2))?-(240+4+22):0; //width+overshot+BorderAndPadding
			special.y=(ev.clientY > (document.documentElement.clientHeight/2))?-(120+4+12):0;

			floater.style.left=(ev.clientX-(offset.left-scrolled.left))+(2+special.x)+"px";
			floater.style.top=(ev.clientY-(offset.top-scrolled.top))+(2+special.y)+"px";
		},
		
		//create a drip system for autolike, instead of an offset
		queAutoLike : function(post){
			var nowTime = timeStamp();
			var lastInQue = WM.likeQueue.last();
			var targetTime = nowTime + (1000*WM.opts.autolikedelay);
			if (lastInQue||null) {
				if (lastInQue.timer>nowTime) {
					targetTime = lastInQue.timer + (1000*WM.opts.autolikedelay);
				}
			}
			WM.likeQueue.push({post:post, timer:targetTime, fn:"like"});
			WM.console.likeQueueCounterNode.textContent = WM.likeQueue.length;
		},
		
		//create a drip system for autolike, instead of an offset
		queAutoComment : function(post,say){
			var nowTime = timeStamp();
			var lastInQue = WM.likeQueue.last();
			var targetTime = nowTime + (1000*WM.opts.autolikedelay);
			if (lastInQue||null) {
				if (lastInQue.timer>nowTime) {
					targetTime = lastInQue.timer + (1000*WM.opts.autolikedelay);
				}
			}
			WM.likeQueue.push({post:post, timer:targetTime, say:say, fn:"comment"});
			WM.console.likeQueueCounterNode.textContent = WM.likeQueue.length;
			//log(["autocomment added",say]);
		},		

		//dump the autolike queue
		emptyAutoLikeQue : function() {
			WM.likeQueue=[];
			WM.console.likeQueueCounterNode.textContent = 0;
		},
		
		//get the next ready autolike target
		checkAutoLikeQue : function() {
			if (WM.likeQueue.length<1) return null;
			var nowTime = timeStamp();
			if (WM.likeQueue[0].timer<=nowTime) {
				WM.console.likeQueueCounterNode.textContent = (WM.likeQueue.length-1);
				var t=nowTime;
				for (var i in WM.likeQueue) {
					i.timer = t;
					t+=(1000*WM.opts.autolikedelay);
				}
				return WM.likeQueue.shift(); // no longer returns the post, but the block of what to do with what post
			}
			return null;
		},

		processPosts : function(){
			//dont run if menu is open or if requests are still out or if the console is paused
			if($("Config") || (WM.requestsOpen >= WM.opts.maxrequests) || WM.paused) return;

			var postNode=selectSingleNode(".//div[starts-with(@id,'post_') and contains(@class,'collect') and not(contains(@class,'paused') or contains(@class,'working'))]",{node:WM.console.feedNode});
			if (postNode) {
				var post = WM.posts[postNode.id.replace('post_','')];
				if (post) post.open();
			}
		},

		olderPosts : function (params) {
			WM.fetch({older:true});
		},

		newerPosts : function (params) {
			WM.fetch({newer:true});
		},
		
		fetchRange : function (params) {
			WM.fetch({bypassPause:true, older:true, targetEdge:params.oldedge, currentEdge:params.newedge});
		},

		cleanPosts : function () {try{
			for (var p in WM.posts) if (!WM.posts[p].isGhost) {
				var post = WM.posts[p];
				with (post) if (!(
					isPinned || isCollect || isWorking ||
					(isTimeout && !WM.opts.cleanTimedOut)
				)) post.remove();
			}
		}catch(e){log("WM.cleanPosts(): "+e);}},
		
		setIntervals : function() {try{
			//setup the timer to try post collection
			if (procIntv) window.clearInterval(procIntv);
			procIntv=window.setInterval(WM.processPosts, 2000);

			//setup the timer to get new posts
			if (newIntv) window.clearInterval(newIntv);
			if(calcTime(WM.opts.newinterval)>0) newIntv=window.setInterval(WM.newerPosts, calcTime(WM.opts.newinterval));

			//setup the timer to get older posts
			if (oldIntv) window.clearInterval(oldIntv);
			if(calcTime(WM.opts.oldinterval)>0) oldIntv=window.setInterval(WM.olderPosts, calcTime(WM.opts.oldinterval)+2000);
			olderLimit=calcTime(WM.opts.maxinterval)||0;

			//setup the timer to clean up old posts from the feed
			if (cleanIntv) window.clearInterval(cleanIntv);
			if(calcTime(WM.opts.cleaninterval)>0) cleanIntv=window.setInterval(WM.cleanPosts, calcTime(WM.opts.cleaninterval)+250);

			//setup global heartbeat
			if (hbIntv) window.clearInterval(hbIntv);
			hbIntv=window.setInterval(WM.onHeartbeat, WM.opts.heartRate);
			
		}catch(e){log("WM.setIntervals: "+e);}},

		hideCounters : function(){try{
			hideNodes("//*[contains(@class,'accFailBlock')]");
		}catch(e){log("WM.hideCounters: "+e);}},

		showCounters : function(){try{
			showNodes("//*[contains(@class,'accFailBlock')]");
		}catch(e){log("WM.showCounters: "+e);}},

		validatePost : function(fbPost){try{
			//validate required post fields
			/*if (!( exists(fbPost.application) && exists(fbPost.link) && fbPost.type=="link")) {
				return;
			}*/

			//accept only posts we have sidekicks for
			var app;
			if (!exists(app=WM.apps[fbPost.app_id])) return;
			
			//prevent redrawing same post in case one slips past the graph validator
			var postID=fbPost.post_id;
			if (WM.posts[postID]||null) return;

			//accept only posts for which a sidekick is enabled
			if (!WM.quickOpts.masterSwitch[app.appID]) return;

			//create a Post object from the post data
			var post=(WM.posts[fbPost]=new WM.Post(fbPost));
			if (post) {
				var hasID=post.identify();
				WM.sortPosts(); //make sure new posts fit the current sort order and direction
				if (hasID) {
					WM.rulesManager.doEvent("onValidate",post);
					WM.rulesManager.doEvent("onIdentify",post);
					post.draw(true,true);
					
					//track the post
					if (WM.opts.useFriendTracker && !post.isMyPost){
						WM.friendTracker.track(post);
					}
				}
			} else {
				log("WM.validatePost: Unable to transform post data into a useful post object. (id:"+fbPost.post_id+")");
			}
		}catch(e){log("WM.validatePost: "+e);}},

		handleEdges : function(params){
			/*
				apps
				friends
				edge:{newer,older}
			*/
			//console.log("handleEdges: "+JSON.stringify(params));
			
			if (params.friends||null) {
				//update user created feeds
				for (var f=0,l=WM.feedManager.feeds.length;f<l;f++){
					var feed = WM.feedManager.feeds[f];
					//if this feed is listed in those passed back...
					if (params.friends.contains(feed.id)){
						//update each app filter in this feed
						for (var c=0,l=params.apps.length;c<l;c++) {
							var appID=params.apps[c];
							filter = feed.filters["app_"+appID];
							if (!(filter||null)) {
								//this filter does not exist, create one
								filter=feed.addFilter({id:"app_"+appID});
							}
							if (params.edge.older) filter.oldedge = params.edge.older;
							if (params.edge.newer) filter.newedge = params.edge.newer;
							filter.oldedgeNode.textContent = filter.oldedge;
							filter.newedgeNode.textContent = filter.newedge;
							if (timeStamp()-(filter.oldedge*1000)>olderLimit) filter.olderLimitReached=true;
						}
					}
				}
			} else {
				//update base feed
				feed = WM.feedManager.feeds[0];
				for (var c=0,l=params.apps.length;c<l;c++) {
					var appID=params.apps[c];
					//update each app filter in this feed
					filter = feed.filters["app_"+appID];
					if (!(filter||null)) {
						//this filter does not exist, create one
						filter=feed.addFilter({id:"app_"+appID});
					}
					if (params.edge.older) filter.oldedge = params.edge.older;
					if (params.edge.newer) filter.newedge = params.edge.newer;
					filter.oldedgeNode.textContent = filter.oldedge;
					filter.newedgeNode.textContent = filter.newedge;
					if (timeStamp()-(filter.oldedge*1000)>olderLimit) filter.olderLimitReached=true;
				}					
			}
		},
				
		fetch : function(params) {try{
			/*
				older:bool
				newer:bool
				apps:[]
				feed:[]
				targetEdge:unixtime
				currentEdge:unixtime
				bypassPause:bool
				bypassFeedDisabled:bool
				bypassAppDisabled:bool
			*/
		
			params=params||{};
			if (WM.fetchPaused && !params.bypassPause) return;

			//convert a single passed app to a single entry list
			if (exists(params.apps) && ((params.apps.objType||null)=="app")) {
				var ret={};
				ret[params.apps.appID]=params.apps;
				params.apps=ret;
			}
			
			var useApps = params.apps||WM.apps;

			//convert a single passed feed to an array
			if (exists(params.feeds) && ((params.feeds.objType||null)=="feed")) {
				params.feeds=[params.feeds];
			}
			params.currentEdge = params.currentEdge||null; //nullify undefined edge
						
			//for each feed individually
			var feeds=params.feeds||WM.feedManager.feeds;
			for (var f=0,len=feeds.length;f<len;f++) { 
				var feed=feeds[f];
				var friend=(feed.url!="https://graph.facebook.com/me/home")?[feed.id]:null;
				
				//ignore the old me feed because it is a duplicate of the wall feed
				if (feed.url!="https://graph.facebook.com/me/feed") if (feed.enabled || params.bypassFeedDisabled) {
								
					//for each app make a separate fetch call for the given feed
					//override this: no more by-app fetching
					
					if (false && !WM.opts.groupFetching && (useApps||null)) {
						for (var a in useApps) {
							var app=useApps[a];
							
							//only fetch for enabled apps
							//where we are fetching new
							//or if we are fetching old we are not at our older limit
							var feedFilter=feed.filters["app_"+a];
							if ((app.enabled || params.bypassAppDisabled) && (feedFilter.enabled || params.bypassFilterDisabled) && !(
									params.older && feedFilter.olderLimitReached
								)
							){
								var G=Graph.fetchPostsFQL_B({
									callback:WM.validatePost,
									direction:(params.newer?1:(params.older?-1:0)),
									limit:WM.opts.fetchQty,
									targetEdge:(params.targetEdge||null), //special for new rules manager actions
									friends:friend,
									apps:[app.appID],
									currentEdge:params.currentEdge||(params.newer?feedFilter.newedge:(params.older?feedFilter.oldedge:null)),
									edgeHandler:WM.handleEdges,
									noAppFiltering:WM.opts.noAppFiltering
									
								});								
							}
						}
						
						
					//join apps together before fetching a single time for the given feed
					} else {
						//get the keys of the apps collection
						var keys=Object.keys(useApps);
						//if any sidekicks are docked
						if (keys.length) {
							//get the values of the apps collection
							var appsToProcess=keys.map(function (key) {
								return useApps[key];
							//filter out which apps are able to be fetched for
							}).filter(function(o,i,p){
								//get the feed filter text
								var feedFilter=feed.filters["app_"+o.appID];
								//get if the app is enabled
								var isEnabled = (o.enabled || params.bypassAppDisabled);
								var isFilterEnabled=true,isOlderLimitReached=false; 
								if (feedFilter||null) {
									//get if the feed filter is enabled
									isFilterEnabled = (feedFilter.enabled || params.bypassFilterDisabled);
									//get if the feed filter has already reached its older edge limit
									isOlderLimitReached = (params.older && feedFilter.olderLimitReached);
								} else {
									//feed filter does not exist for this app
									//assume it was deleted by the user on purpose
									//and don't fetch for this app on this feed
									log("WM.fetch: could not find filter for " + o.appID + "in feed " + feed.id);
									return false;
								}
								if (isEnabled && isFilterEnabled && !isOlderLimitReached) return true;
								return false;
							//simply the array
							}).map(function(o,i,p){
								//just get the id's of apps to do, not the entire app object
								return o.appID;
							});
							
							//make sure we matched filters to process
							if (appsToProcess.length){
								//get the shared edges of the passed apps
								var edges = feed.getMergedEdges({apps:appsToProcess});
								//console.log("getMergedEdges returned: "+JSON.stringify(edges));
								
								var G=Graph.fetchPostsFQL_B({
									callback:WM.validatePost,
									direction:(params.newer?1:(params.older?-1:0)),
									limit:WM.opts.fetchQty,
									targetEdge:(params.targetEdge||null), //special for new rules manager actions
									friends:friend,
									apps:appsToProcess,
									currentEdge:params.currentEdge||(params.newer?edges.newedge:(params.older?edges.oldedge:null)),
									edgeHandler:WM.handleEdges,
									noAppFiltering:WM.opts.noAppFiltering
								});
							}
						}
					}
					
					
				}
			}
		}catch(e){log("WM.fetch: "+e);}},
		
		changeDebugSettings : function(){try{
			if (debug && debug.initialized) {
				debug.doDebug = WM.opts.debug;
				debug.debugLevel = parseInt(WM.opts.debugLevel);
				debug.debugMaxComments = WM.opts.debugMaxComments;
				debug.useScrollIntoView = WM.opts.debugScrollIntoView;
				debug.stackRepeats = WM.opts.debugStackRepeats;
			} else {
				if (debug) debug.init();
				setTimeout(WM.changeDebugSettings,1000);
			}
		}catch(e){log("WM.changeDebugSettings: "+e);}},
				
		changeConfigSettings : function(){try{
			WM.config.sectionsAsTabs=WM.opts.configSectionsAsTabs;
			WM.config.separatorsAsTabs=WM.opts.configSeparatorsAsTabs;
			WM.config.useScrollIntoView=WM.opts.configScrollIntoView;
			WM.config.confirms={
				save:WM.opts.configConfirmSave,
				cancel:WM.opts.configConfirmCancel,
				"import":WM.opts.configConfirmImport,
				restore:WM.opts.configConfirmRestore
			};
		}catch(e){log("WM.changeConfigSettings: "+e);}},

		resizeConsole : function(){try{
			//negotiate height with fb bluebar
			var node=$("pagelet_bluebar");
			var h=(node)?elementOuterHeight(node):0;
			
			with($("wmContent")){
				style.height=document.documentElement.offsetHeight-h+"px";
				style.width=document.documentElement.offsetWidth+"px";
			}
			WM.console.tabContainer.redraw();
			WM.console.collectTabControl.redraw();

		}catch(e){log("WM.resizeConsole: "+e);}},

		setColors : function(){try{
			var colors=["excluded","working","timeout","paused","nodef","failed","accepted","scam","pinned"];
			var css="";
			for (var c=0, color; (color=colors[c]); c++) {
				css+=("div."+color+"{background-color:"+WM.opts["colors"+color]+" !important;}\n");
			}
			//set the new transition delay timer
			css+=(".wm.post.short:hover .floater {-moz-transition-property: padding,border,width,height;-moz-transition-delay:"+WM.opts["transitiondelay"]+"s; width:240px; padding:5px 10px;border:1px solid;}\n");
			remove($("user_colors_css"));
			addGlobalStyle(css,"user_colors_css");
		}catch(e){log("WM.setColors: "+e);}},

		initConsole : function(){try{
			WM.console.loading=false;
			if (WM.console.initialized) log("WM Console Initialized");

			//show options menu button
			with (WM.console.configButton) {
				className = className.removeWord("jsfHidden");
			}

			//set console heights
			WM.resizeConsole();

			//load feed sources
			WM.feedManager.init();
			
			//import friend tracker data
			//and delete posts out of bounds with our "track for how many days"
			WM.friendTracker.init();
			WM.friendTracker.clean();

			//initialize user colors
			WM.setColors();
			
			//set up the priorities and limits object
			//and new rules manager
			WM.rulesManager.init();

			//decipher the dynamic tests
			WM.grabber.init();

			//show counters
			if (WM.opts.showcounters) WM.showCounters(); else WM.hideCounters();
			
			//set intervals
			WM.setIntervals();
			
			//set autopause
			if (WM.opts.autopausecollect) WM.pauseCollecting(true);
			if (WM.opts.autopausefetch) WM.pauseFetching(true);
			
			//open a channel for sidekick communication
			WM.fetchSidekickData();

			//add an entrypoint for sidekicks since we know FB gave us access
			var createDock = function(){
				document.body.appendChild(
					createElement('div',{id:'wmDock',style:'display:none;',onclick:function(){
						WM.dock.answerDockingDoor();
					}})
				);
				document.body.appendChild(
					createElement('div',{id:'wmDataDump',style:'display:none;'})
				);
			};
			
			createDock();			

		}catch(e){log("WM.initConsole: "+e);}},

		cleanHistory : function(params){try{
			log("Cleaning History");
			params=params||{};
			var ask=WM.opts.historyConfirmClean;
			if (params.noConfirm || !ask || (ask && confirm("Clean and pack history for this profile?"))){
				//history = getOptJSON("history_"+WM.currentUser.profile)||{};
				var ageDays=parseInt(WM.opts.itemage);
				var timeNow=timeStamp();
				for(var i in WM.history) {
					if( ( (timeNow-WM.history[i].date) /day) > ageDays) {
						delete WM.history[i];
					}
				}
				setOptJSON("history_"+WM.currentUser.profile, WM.history);
			}
		}catch(e){log("WM.cleanHistory: "+e);}},

		optionsSetup : function(){try{
			debug.print("WM.optionsSetup:");

			//create the settings tree
			WM.config = new Config({
				storageName:"settings_"+(WM.quickOpts.useGlobalSettings?"global":WM.currentUser.profile),
				onSave:WM.onSave,
				title:"FB Wall Manager "+WM.version+(WM.quickOpts.useGlobalSettings?" (!! Global Settings !!)":""),
				logo:createElement("span",{}[
					createElement("img",{className:"logo",src:"",textContent:"v"+WM.version}),
					createElement("text","v"+WM.version)
				]),
				css:(
					WM.console.dynamicIcons()+
					jsForms.globalStyle()
				),
				settings:{
					btn_useGlobal:{
						type:"button",
						label:"Use Global Settings", 
						title:"Switch to using a global storage for settings. Those settings can then be used by other accounts (not browser profiles).",
						script:function(){
							if (WM.quickOpts.useGlobalSettings||false) {
								//already using global settings
								return;
							}
							if (confirm("Switch to using global (shared) settings?")){
								WM.quickOpts.useGlobalSettings=true;
								WM.saveQuickOpts();
								WM.config.title = "FB Wall Manager "+WM.version+" (!! Global Settings !!))";
								WM.config.storageName = "settings_global";
								WM.config.values=WM.config.read();
								WM.config.configure();
								WM.config.reload();
							}
						},
					},
					btn_useOwnProfile:{
						type:"button",
						label:"Use Profile Settings", 
						title:"Switch to using your own profile storage for settings.",
						script:function(){
							if (!(WM.quickOpts.useGlobalSettings||false)) {
								//already using profile settings
								return;
							}
							if (confirm("Switch to using your own profile settings?")){
								WM.quickOpts.useGlobalSettings=false;
								WM.saveQuickOpts();
								WM.config.title = "FB Wall Manager "+WM.version;
								WM.config.storageName = "settings_"+WM.currentUser.profile;
								WM.config.values=WM.config.read();
								WM.config.configure();
								WM.config.reload();
							}
						},
					},
				
					wmtab_opts:tabSection("Host Options",{
					
						section_basicopts:section("Basics",{
							/*authTokenTools:optionBlock("Authorization",{
								devAuthToken:checkBox("Automatically check my developer tool app for my Auth Token"),									
							},true),*/
						
							intervals:optionBlock("Post Fetching",{								
								newinterval:{
									label:"Get Newer Posts Interval",
									type:"selecttime",
									title:"Fetch new posts from facebook after a set time.",
									options:{
										"off":"Off",
										"tenth":"6 seconds",
										"sixth":"10 seconds",
										"half":"30 seconds",
										"one":"1 minute",
										"two":"2 minutes",
										"three":"3 minutes",
										"four":"4 minutes",
										"five":"5 minutes",
										"ten":"10 minutes",
									},
									"default":"t:30s"
								},

								fetchQty:{
									label:"Fetch how many? (subject to filtering)",
									type:"select",
									title:"Posts fetched per request. Higher numbers affect speed of fetching.",
									options:{
										"5":"5",
										"10":"10",
										"25":"25",
										"50":"50",
										"100":"100",
										"250":"250",
										"500":"500 (FB maximum)", //known maximum fetch as of 9/8/2013
									},
									"default":"25"
								},

								oldinterval:{
									label:"Get Older Posts Interval",
									type:"selecttime",
									title:"Fetch previous posts from facebook after a set time.",
									options:{
										"off":"Off",
										"tenth":"6 seconds",
										"sixth":"10 seconds",
										"half":"30 seconds",
										"one":"1 minute",
										"two":"2 minutes",
										"three":"3 minutes",
										"four":"4 minutes",
										"five":"5 minutes",
										"ten":"10 minutes",
									},
									"default":"off"
								},

								maxinterval:{
									label:"How old is too old?",
									type:"selecttime",
									title:"Tell WM what you think is a good max post age to fetch. Also affects which posts are considered 'stale'.",
									options:{
										"off":"Off/Infinite",
										"hour":"1",
										"2hour":"2",
										"3hour":"3",
										"4hour":"4",
										"8hour":"8",
										"12hour":"12",
										"18hour":"18",
										"24hour":"24",
										"32hour":"32",
										"48hour":"48",
									},
									"default":"t:1d"
								},

								groupFetching:checkBox("All installed sidekicks in one request (default: one request per sidekick)",false,{},true),
								noAppFiltering:checkBox("Have WM filter posts for you instead of having facebook do it (may prevent some empty data set issues)",false,{},true),
							},true),

							autoPauseBlock:optionBlock("Fetching/Collecting Autopause",{
								autopausefetch:checkBox("Pause Fetching after First Fetch"),
								autopausecollect:checkBox("Pause Collection on Startup"),
							},true),

							multiTaskBlock:optionBlock("Multi-task",{
								maxrequests:inputBox("Max requests simultaneously",1),
								recycletabs:inputBox("Recycle Windows/Tabs",1),
								recycletabsall:checkBox("Recycle All",true),
							},true),

							queBlock:optionBlock("Task-Queue",{
								queuetabs:checkBox("Force all posts and autolikes through one tab using a queue (overrides multi-task)",true),
							},true),

							timeoutBlock:optionBlock("Time-outs",{
								reqtimeout:inputBox("Item Acceptance Page Timeout (seconds)",30),
								failontimeout:checkBox("Mark Timeout as Failure (default: retry indefinitely)"),
							},true),
						}),

						section_access:section("Accessibility",{
							shortModeBlock:optionBlock("Short Mode",{
								thumbsize:{
									label:"Thumbnail Size",
									type:"select",
									title:"Size of bonus thumbnails in display mode: short and .",
									options:{
										"mosquito":"16px",
										"tiny":"24px",
										"small":"32px",
										"medium":"48px",
										"large":"64px",
										"xlarge":"96px",
									},
									"default":"medium"
								},
								transitiondelay:inputBox("Hover Box Delay (s)",1),
							},true),

							accessTweaksBlock:optionBlock("Tweaks",{
								debugrecog:checkBox("Show Identified Text (instead of original link text)",true),
								showcounters:checkBox("Show Accept/Fail Counts",true),
								showdynamictips:checkBox("Show Dynamic Console Tips",true),
								appsConfirmDeleteUDT:checkBox("Confirm Delete User Defined Types",true),
							},true),

							toolBoxBlock:optionBlock("Customize Post Toolbox",{
								showtoolbox:checkBox("Enable ToolBox", true),

								showopen:checkBox("Open Post",true),
								showmarkfailed:checkBox("Mark As Failed",true),
								showmarkaccepted:checkBox("Mark As Accepted",true),
								showlike:checkBox("Like Post",true),
								showreid:checkBox("Re-ID Post",true),
								showmovetop:checkBox("Move to Top",true),
								showmovebottom:checkBox("Move to Bottom",true),
								showpin:checkBox("Pin Post",true),
								showclean:checkBox("Clean Post",true),
								showpostsrc:checkBox("Show Post Source",true),

								//new stuff
								showcancelprocess:checkBox("Cancel Process or Like",true),
								showrestartprocess:checkBox("Restart Process or Like",true),
								showpausetype:checkBox("Pause Bonus Type",true),
								showunpausetype:checkBox("Unpause Bonus Type",true),
								showaddfeed:checkBox("Add To Feeds",true),

								showmakerule:checkBox("Rule From Post",true),
								showoriginaldata:checkBox("Show Original Data",true),
								showautocomment:checkBox("Auto Comment",true),
								
							},true),
							
							littleToolBoxBlock:optionBlock("Customize Mini Toolbox",{
								littleButtonSize:{
									label:"Mini Toolbutton Size (requires refresh to redraw)",
									type:"select",
									title:"Size of buttons on mini toolbars",
									options:{
										"16":"16px",
										"24":"24px",
										"32":"32px",
									},
									"default":"24",
								},
							},true),

							userColorsBlock:optionBlock("Colors",{
								colorsaccepted:colorBox("Accepted","limegreen"),
								colorsfailed:colorBox("Failed","red"),
								colorsworking:colorBox("Working","yellow"),
								colorsexcluded:colorBox("Excluded","gray"),
								colorspaused:colorBox("Paused","silver"),
								colorsnodef:colorBox("No Definition","deepskyblue"),
								colorsscam:colorBox("Potential Scam","purple"),
								colorspinned:colorBox("Pinned","black"),
								colorstimeout:colorBox("Timeout","orange"),
							},true),
							

						}),

						section_feedback:section("Feedback",{
							publishwarning:{type:"message",title:"Autolike has changed",textContent:"As of WM beta 40 you must allow 'publish_actions' on the 'user data permissions' tab in your Graph API Explorer token builder.",newitem:true},
							gotoapiexplorer:anchor("Visit API Explorer","http://developers.facebook.com/tools/explorer?&version=v1.0"),
							autoSetup:optionBlock("Setup",{
								useautocomment:checkBox("Use Auto-comment (experimental)"),
								useautolike:checkBox("Use Auto-like"),
								//autoliketimeout:inputBox("Timeout (seconds)",30),
								autolikedelay:inputBox("Ban-Prevention Delay (seconds)",3),
							},true),
							autoLikeBlock:optionBlock("Perform Feedback For",{
								autolikeall:checkBox("All Posts"),
								autolikeaccepted:checkBox("Accepted Posts"),
								autolikesent:checkBox("Sent Posts"),
							},true),
							autoCommentListBlock:optionBlock("Comments (experimental)",{
								autocommentlist:textArea("Random Comments (One per line)","Thanks\nThank you\nthanks"),
							},true),
							blockautolikebygame:optionBlock("Block Feedback by Game",{},false),
						}),

						section_filters:section("Filters",{
							displayfilters:optionBlock("Remove Feed Parts (Classic Mode Only)",{
								hideimages:checkBox("Images (All)"),
								hideimagesunwanted:checkBox("Images (Unwanted Posts)"),
								hidebody:checkBox("Post Body Text"),
								hidevia:checkBox("Via App"),
								hidedate:checkBox("Date/Time"),
							},true),

							filters:optionBlock("Hide By Type",{
								hidemyposts:checkBox("My Posts"),
								hideunwanted:checkBox("Unwanted"),
								hideaccepted:checkBox("Accepted"),
								hidefailed:checkBox("Failed"),
								hidescams:checkBox("Scams"),
								hidestale:checkBox("Stale Posts"),
								hideexcluded:checkBox("Excluded"),
								hideliked:checkBox("Liked By Me"),
								hideunsupported:checkBox("Unsupported Apps"),

								donthidewishlists:checkBox("Don't Hide Known Wish Lists"),
							}),

							//allow hiding all posts by particular games
							filterapps:optionBlock("Hide By App",{}),

							//now added dynamically as appID+"dontsteal"
							dontstealBlock:optionBlock("Don't take W2W posts not for me",{}),

							skipopts:optionBlock("Skip By Type",{
								skipliked:checkBox("Liked By Me"),
								skipstale:checkBox("Day-Old Posts"),
							}),

							filterTweaksBlock:optionBlock("Tweaks",{
								accepton15:checkBox("Mark 'Unrecognized Response' As Accepted"),
								markliked:checkBox("Mark Liked As Accepted (must check Skip Liked)"),
							},true),

							filterCleanupBlock:optionBlock("Cleanup",{
								cleaninterval:{
									label:"Cleanup Interval",
									type:"selecttime",
									title:"Remove unwanted posts from collection console after a set time.",
									options:{
										"off":"Off",
										"one":"1 minute",
										"two":"2 minutes",
										"five":"5 minutes",
										"ten":"10 minutes",
										"fifteen":"15 minutes",
										"thirty":"30 minutes",
										"hour":"1 hour",
									},
									"default":"off"
								},
								cleanTimedOut:checkBox("Clean timed out posts",true),
							},true),
						}),

						section_history:section("History",{
							itemage:inputBox("How long to keep tried items in memory (days)",2),
							oblock_historyConfirms:optionBlock("Confirm (Changes available on next config open)",{
								historyConfirmClear:{type:"checkbox",label:"Clear History",title:"Confirm before clearing history.","default":true},
							},true),
							reset:button("Clear History",
								WM.resetAccepted
							),
						}),

						section_feedopts:section("Feeds Manager",{
							oblock_feedsConfirms:optionBlock("Confirm",{
								feedsConfirmDeleteFeed:{type:"checkbox",label:"Delete Rule",title:"Require confirmation to delete a feed.","default":true},
							},true),
						}),

						section_dynamicopts:section("Dynamic Grabber",{
							dynamicopts:optionBlock("Dynamic Collection",{
								dynamicFirst:checkBox("Run Dynamics BEFORE Sidekicks",true),
							},true),
							enableDynamic:optionBlock("Enable Dynamics by Game",{}),
							oblock_dynamicConfirms:optionBlock("Confirm",{
								dynamicConfirmDeleteTest:{type:"checkbox",label:"Delete Rule",title:"Require confirmation to delete a test.","default":true},
							},true),
						}),

						section_friendtrackopts:section("Friend Tracker",{
							useFriendTracker:checkBox("Enable Friend Tracking",true),
							trackTime:inputBox("Track For How Many Days",2),
							trackeropts:optionBlock("Track Data",{
								trackCreated:checkBox("Post Creation Counts",true),
								trackLastKnownPost:checkBox("Last Known Post Time",true),
								trackAccepted:checkBox("Bonuses Accepted",true),
								trackFailed:checkBox("Bonuses Failed",true),
							},true),
							oblock_trackerConfirms:optionBlock("Confirm",{
								trackConfirmClearUser:{type:"checkbox",label:"Clear User Data",title:"Require confirmation to clear user data.","default":true},
							},true),
						}),
						
						section_rulesopts:section("Rules Manager",{
							oblock_rulesHeartbeat:optionBlock("Heartbeat",{
								heartRate:inputBox("Global Heartbeat Delay (ms)",1000),
								heartbeatAffectsApps:{type:"checkbox",label:"Affect Apps",title:"Heartbeat can be heard at app level on every rule at once. This can slow down your system."},
								heartbeatAffectsPosts:{type:"checkbox",label:"Affect Posts",title:"Heartbeat can be heard at post level on every rule at once. This can slow down your system."},
								heartbeatAffectsRules:{type:"checkbox",label:"Affect Rules",title:"Heartbeat can be heard at rule level on every rule at once. This can slow down your system."},
								heartbeatAffectsFeeds:{type:"checkbox",label:"Affect Feeds",title:"Heartbeat can be heard at feed level on every rule at once. This can slow down your system."},
								heartbeatAffectsFeedFilters:{type:"checkbox",label:"Affect Feed Filters",title:"Heartbeat can be heard at feed filter level on every rule at once. This can slow down your system."},
							},true),
							oblock_rulesConfirms:optionBlock("Confirm",{
								rulesConfirmDeleteValidator:{type:"checkbox",label:"Delete Validator",title:"Require confirmation to delete a rule's validator.","default":true},
								rulesConfirmDeleteAction:{type:"checkbox",label:"Delete Action",title:"Require confirmation to delete a rule's action.","default":true},
								rulesConfirmDeleteRule:{type:"checkbox",label:"Delete Rule",title:"Require confirmation to delete a rule.","default":true},
								rulesConfirmResetLimit:{type:"checkbox",label:"Reset Limit",title:"Require confirmation to reset individual limits.","default":true},
								rulesConfirmResetAllLimits:{type:"checkbox",label:"Reset All Limits",title:"Require confirmation to reset all limits.","default":true},
								rulesConfirmHatch:{type:"checkbox",label:"Hatch Eggs",title:"Require confirmation to hatch eggs.","default":true},
							},true),
							rulesJumpToNewRule:{type:"checkbox",label:"Jump To New Rules",title:"When new rules are created from tests or posts, select the rules manager tab and scroll the new rule into view.","default":true},
						}),

						section_dev:section("Debug",{
							oblock_debugTweaks:optionBlock("Tweaks",{
								pinundefined:checkBox("Pin Undefined Bonus Types"),
							},true),
							debugOpts:optionBlock("Debug",{
								debug:checkBox("Enable Debug",true),
								debugLevel:{
									label:"Debug Sensitivity",
									title:"Sets the level of errors and warnings to report. 0 is all, 5 shows only the worst.",
									type:"select",
									options:{
										"0":"Function calls",
										"1":"Function subsections & debug notes",
										"2":"Captured expected errors",
										"3":"Known open errors",
										"4":"Unexpected errors",
										"5":"Fatal errors",
									},
									"default":"0"
								},
								debugMaxComments:inputBox("Max debug lines (0 for no limit)",100),
								debugScrollIntoView:checkBox("Use scrollIntoView"),
								debugStackRepeats:checkBox("Stack Immediate Repeats"),

							},true),
							advDebugOpts:optionBlock("Advanced Debug",{
								devDebugFunctionSubsections:checkBox("Debug Function Subsections",false),
								devDebugGraphData:checkBox("Debug Graph Packets (not available for Chrome)",false),
							},true),
							GM_special:optionBlock("Script-runner Options",{
								useGM_openInTab:checkBox("Use GM_openInTab instead of window.open",false),
							},true),

						}),

						section_configopts:section("Config",{
							oblock_configConfirms:optionBlock("Confirm (Changes available on next config open)",{
								configConfirmSave:{type:"checkbox",label:"Save",title:"Confirm before saving settings.","default":true},
								configConfirmCancel:{type:"checkbox",label:"Cancel",title:"Confirm before closing settings without saving.","default":true},
								configConfirmImport:{type:"checkbox",label:"Import",title:"Confirm before importing settings.","default":true},
								configConfirmRestore:{type:"checkbox",label:"Restore Defaults",title:"Confirm before restoring defaults.","default":true},
							},true),
							oblock_configStyling:optionBlock("Styling (Changes available on next config open)",{
								configSectionsAsTabs:{type:"checkbox",label:"Display Sections as Tabs",title:"Converts top level roll-outs only. Display those rollouts as tabs on next open of config."},
								configSeparatorsAsTabs:{type:"checkbox",label:"Display Separators as Tabs",title:"Converts second level roll-outs only. Display those rollouts as tabs on next open of config. Removes select all/none buttons on top of the separator."},
							},true),
							oblock_configTweaks:optionBlock("Tweaks (Changes available on next config open)",{
								configScrollIntoView:{type:"checkbox",label:"Use scrollIntoView",title:"When tabs and sections are opened, use the scrollIntoView function to bring them more fully into view. This is jerky at best."},
							},true),
						}),
					}),

					wmtab_games:tabSection("Sidekick Options",{
						skmovedwarning:{type:"message",title:"Sidekick options have moved",textContent:"Sidekick options have been moved to separate config windows. Access them by using the 'Manage Sidekicks' tab, where you can find new 'Options' buttons for each sidekick."},
					}),

					wmtab_info:tabSection("Info",{
						MainMessageCenter:separator("Documentation - Messages - Help",null,{
							Mainupdate:anchor("Update Script","http://userscripts.org/scripts/source/86674.user.js"),
							donateWM:{type:"link",label:"Donate for FBWM via Paypal",href:"https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=merricksdad%40gmail%2ecom&lc=US&item_name=Charlie%20Ewing&item_number=FBWM&currency_code=USD&bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHosted"},
							Mainwikipage:anchor("Wiki Support Page","http://fbwm.wikia.com/wiki/Known_Issues"),
							Mainsetupinfo:anchor("Setup Info","http://fbwm.wikia.com/wiki/New_User_Setup"),
							Maindiscuss:anchor("Known Bugs","http://fbwm.wikia.com/wiki/Known_Issues"),
							Mainrevisionlog:anchor("Revision Log","http://fbwm.wikia.com/wiki/Revisions"),
						},true),
					}),
					
					wmtab_scripts:tabSection("Get More!",{
						
					}),

				},
			});
			
			// add options shortcut to user script commands
			GM_registerMenuCommand("Wall Manager "+WM.version+" Options", function(){WM.config.open();});
		}catch(e){log("WM.optionsSetup: "+e);}},

		init : function(){try{
			//capture user id/alias/name and make it global
			WM.currentUser.id = Graph.userID;
			WM.currentUser.alias = Graph.userAlias;
			WM.currentUser.profile = WM.currentUser.alias||WM.currentUser.id;

			debug.print("UserID:"+WM.currentUser.id+"; UserAlias:"+WM.currentUser.alias+"; WM is Using:"+WM.currentUser.profile);

			//get WM.quickOpts
			WM.quickOpts = getOptJSON('quickopts_'+WM.currentUser.profile)||{};
			WM.quickOpts["filterApp"]=(WM.quickOpts["filterApp"]||"All");
			WM.quickOpts["displayMode"]=(WM.quickOpts["displayMode"]||"0");
			WM.quickOpts["masterSwitch"]=(WM.quickOpts["masterSwitch"]||{});
			WM.quickOpts["useGlobalSettings"]=(WM.quickOpts["useGlobalSettings"]||false);

			//create the options menu
			WM.optionsSetup();

			//duplicate the options saved in WM.config
			WM.updateSettingsValues();

			//set up the config with its internal special variables
			WM.changeConfigSettings();

			//setup debug beyond its defaults
			WM.changeDebugSettings();
			
			//clean history
			WM.history = getOptJSON('history_'+WM.currentUser.profile)||{};
			WM.cleanHistory();
						
			//prep the console now that we have an id and/or alias
			//and then carry on with our init
			WM.console.init({callback:WM.initConsole});
		}catch(e){log("WM.init: "+e);}},
		
		receiveSidekickMessage: function(event) {
			if (isObject(event.data)) {
				var data=event.data; //just shorten the typing
				if (data.channel=="WallManager"){
					//log(JSON.stringify(data));
					//$("WM_debugWindow").childNodes[1].lastChild.scrollIntoView();
					switch (data.msg){
						case 2: //getting a comOpen response from sidekick
							//WM.collector.tabs[data.tabID].comOpen=true;
							break;
							
						case 4: //getting a status package from sidekick
							switch (data.params.action){
								case "onFrameLoad":
									WM.onFrameLoad(data.params);
									break;
								case "onFrameLoad3":
									WM.onFrameLoad3(data.params);
									break;
							}
							break;
					}
				}
			}
		},		

		run : function() {try{
			// pre-load console images
			//for(var img in imgs) try{new Image().src = imgs[img];}catch(e){log("preload: "+e);}

			//special about:config entry for disabling storage of fb auth token
			//should help multi account users
			//if (getOpt("disableSaveAuthToken")) 
			Graph.authToken=null;
			
			//patch 38 auth token stuff
			var flagManualAuthSuccessful=getOpt("flagManualAuthSuccessful")||false;					
			if (WallManager.switches.manualAuthToken && !flagManualAuthSuccessful) {
				var m="WM can no longer access your FB Access Token without your manual assistance.\nTo successfully fetch posts, please complete the following:\n\n*In a new browser window, visit: http://developers.facebook.com/tools/explorer\n\n*If required, allow that app access to your facebook information\n*Find the 'Get Access Token' button and click it\n*In the panel that appears, click 'extended permissions'\n*Be sure that 'read_stream' is selected or otherwise not blank\n*If you want to use autolike/autocomment also select 'publish_actions' from the 'user data permissions' tab*Click the 'Get Access Token' button\n*Now find the box that says 'Access Token' and select its value\n*Copy that value and paste it into the box on this promp\n\nNote: this token does not last forever, you may need to repeat this process";
				var manualToken = prompt(m,"paste token here");
				
				//validate manualToken at least somewhat
				
				//halt if manual token is not given
				if (manualToken=="" || manualToken==null || manualToken=="paste token here") {
					alert("manual token not accepted, please refresh and try again");
					return;
				}
				
				//pass the manual token along
				Graph.authToken=manualToken;
				
				//consider saving time by looking for auth tokens automatically from here out
				var m = "WM thinks your auth token setup is successful.\nIf you like, I can make it so WM just checks your dev tool for new auth tokens every time.\n\nPress Cancel to continue entering auth codes manually.\n\n*If you have multiple facebook accounts on this computer using WM, please make sure you set up the API explorer with every account.";
				var saveProgress = confirm(m);
				if (saveProgress) {
					setOpt("flagManualAuthSuccessful",true);					
				}
			} 

			var G=Graph.fetchUser({callback:WM.init});
			
			if (G){if (G.requestAlreadyOut) {
			} else if (G.initRequestSlow) {
			} else if (G.olderLimitReached) {
			} else if (G.getAuthTokenFailed) {
			}}
			
		}catch(e){log("WM.run: "+e);}}		
	};

	var WM=WallManager;

	//returns the current date-time in unix format, not localized
	WM.__defineGetter__("currentTime",function(){try{
		return timeStamp();
	}catch(e){log("WM.currentTime: "+e);}});
	
	//returns the appID of the selected app tab on the collection panel, or 'all' if 'Show All' is selected
	WM.__defineGetter__("currentAppTab",function(){try{
		var tabCtrl=WM.console.collectTabControl;
		if (tabCtrl||null) {
			var tab = tabCtrl._selectedTab;
			if (tab||null) return tab.appFilter;
		}
		return "all";
	}catch(e){log("WM.currentAppTab: "+e);}});

	var sandbox=this;
	
	//allow certain options to be seen outside of the WallManager object
	//graph extension is external but still calls on WM options if they exist
	opts=WM.opts;
	quickOpts=WM.quickOpts;
					
//***************************************************************************************************************************************
//***** global functions
//***************************************************************************************************************************************
	sandbox.isNumber = function(o){return ((typeof o) == "number");};
	sandbox.isArray = function(o) {return Object.prototype.toString.call(o) === "[object Array]";};

//***************************************************************************************************************************************
//***** Visual Console Object
//***************************************************************************************************************************************
	WM.console = {
		initialized: false,
		sidekickNode: null, //remember the sidekicks list
		feedNode: null, //remember where to put the feed data
		loading: true, //set to false after sidekicks have time to load
		priorityNode: null,
		priorityBuild: null,
		dynamicBuild: null,
		
		//new content
		tabContainer:null, //outer tab control
		configButton:null, //userConfig.open control
		collectTabControl:null, //app filter tab control
		
		dynamicIcons: function(){
			//define a crapload of icons
			var icons={
				//128x128 pixel icons
				row0:["refresh","world","check",null,"moveUpLevelLeft","moveUpLevelRight","moveDownLevelLeft","moveDownLevelRight","filter","plus","minus","multiply","import","reset","object","array"],
				row1:["expandDown","expandUp","expandLeft","expandRight","moveTopLeft","moveBottomLeft",null,"allSidekicks","location","sortAsc","sortDesc","tools","treeExpand","treeCollapse","exportGrab","grab"],
				row2:["playDown","playUp","playLeft","playRight","like","unlike","uncheckAll","checkAll","layoutSmall","layoutDetail","layoutList","sidekick","refreshProcess","cancelProcess","importData","heartbeat"],
				row3:["arrowDown","arrowUp","arrowRight","arrowLeft","rssUpRight","rssUpLeft","rssDownRight","rssDownLeft","pin","pinned","redPhone","shuffle",null,"birth","comment"],
				row4:["plugin","identify","add","remove","openInNewWindow","restoreDown","stop","pause","trash","action","logo",null,"moveOutLevel","moveInLevel","removeGlobal","toGlobal"],
				row5:["clone","hatch","tag","noImage","accordionExpandH","accordionCollapseH","accordionExpandV","accordionCollapseV","gotoLibrary","addFilter","removeFilter","maximize","addFeed","addGlobal","fromGlobal","checkGlobal"],
				
				//32px icons
				row6:["firefox","chrome",null,"tabs"],
				
				//16px icons
				row7:["treeCollapseS","treeExpandS","layoutSmallColor","layoutDetailColor","layoutListColor",null,null,null,null,null,null,"noImageSmall"],
			};
			
			var ret=".resourceIcon {display:block; background-image:url('"+WM.resources.iconsURL+"') !important;}\n";
			
			//create css statements 
			//for rows 0-5,6,7
			var sizes=[8,16,24,32,48,64];
			
			for (var si=0,len=sizes.length;si<len;si++){
				var s=sizes[si];
				for (var r=0;r<=6;r++){
					for (var i=0;i<20;i++){
						var iconName=icons["row"+r][i];
						if (iconName!=null) {
							ret+="."+iconName+s+" {background-position:"+(-i*s)+"px "+(-r*s)+"px; width:"+s+"px; height:"+s+"px; background-size:"+(1024/(64/s))+"px;}\n";
						}
					}
				}
				r=6;
				for (var i=0;i<20;i++){
					var iconName=icons["row"+r][i];
					if (iconName!=null) {
						//6 rows of icons 2 times this size
						var yOffset=(6*s*2);
						ret+="."+iconName+s+" {background-position:"+(-(i*s))+"px "+(-yOffset)+"px; width:"+s+"px; height:"+s+"px; background-size:"+(1024/(64/(s*2)))+"px;}\n";
					}
				}
				r=7;
				for (var i=0;i<20;i++){
					var iconName=icons["row"+r][i];
					if (iconName!=null) {
						//6 rows of icons 4 times this size
						//plus 1 row of icons twice this size
						var yOffset=(6*s*4) + (1*s*2);
						ret+="."+iconName+s+" {background-position:"+(-(i*s))+"px "+(-yOffset)+"px; width:"+s+"px; height:"+s+"px; background-size:"+(1024/(64/(s*4)))+"px;}\n";
					}
				}
			}

			return ret;
		},
		
		globalStyle:function(){try{
			return ""+
			
			//icon sheets
			WM.console.dynamicIcons()+
			
			"html {height:100%; width:100%;}\n"+
			"body {margin:0 !important; font-family:tahoma,arial; font-size:small;}\n"+
			
			"a:hover {text-decoration: none !important;}\n"+

			"#content {display:none !important; }\n"+

			"#wmContent {background-color:#DDDDEE; position:relative;}\n"+

			".post.classic {position:relative; min-height:90px; border-bottom:1px solid #CCCCDD; padding-bottom:10px; padding-top:10px; clear:both;}\n"+
			".post.classic .actor {margin-top:5px; margin-bottom:10px; font-weight:700; color:#3B5998; display:inline;}\n"+
			".post.classic .picture {padding-top:5px; padding-right:10px; float:left;}\n"+
			".post.classic .picture img {width:90px; height:90px; background-color:white; border:1px solid; border-radius:5px;}\n"+
			".post.classic .body {vertical-align:top;}\n"+
			".post.classic .title {margin-top:5px; font-weight:700; color:#3B5998;display:block;}\n"+
			".post.classic .caption {display:block; }\n"+
			".post.classic .description {padding-top:5px; display:block;}\n"+
			".post.classic .postDate {}\n"+
			".post.classic .appName {position:relative; left:10px;}\n"+
			".post.classic .linkText {color:#899ADB; float:right; padding-right:32px;}\n"+
			".post.classic.noimage {min-height:1px;}\n"+
			
			".post.short {float:left; position:relative;}\n"+
			".post.short .floater {overflow:hidden; display:block; background-color: white; border:0px solid; border-radius:5px; position:absolute; z-index:3; padding:0; width:0px;}\n"+
			".post.short:hover .floater {-moz-transition-property: width,height,padding,border;-moz-transition-delay:1s; width:240px; padding:5px 10px;border:1px solid;}\n"+
			".post.short .actor {display:block;}\n"+
			".post.short .picture {position:relative;}\n"+
			".post.short .picture img {position:relative; width:100%; height:100%; background-color:white;}\n"+
			".post.short .postDate {display:block;}\n"+
			".post.short .appName {display:block;}\n"+
			".post.short .linkText {display:block;}\n"+
			".post.short .progress {opacity:0.25; background-color:#00FF00;}\n"+

			".post.short.working .picture img {opacity:0.25;}\n"+
			".post.short.excluded .picture img {opacity:0.25;}\n"+
			".post.short.timeout .picture img {opacity:0.25;}\n"+
			".post.short.paused .picture img {opacity:0.25;}\n"+
			".post.short.nodef .picture img {opacity:0.25;}\n"+
			".post.short.accepted .picture img {opacity:0.25;}\n"+
			".post.short.failed .picture img {opacity:0.25;}\n"+
			".post.short.colored .picture img {opacity:0.25;}\n"+
			".post.short.scam .picture img {opacity:0.25;}\n"+
			".post.short.pinned .picture img {opacity:0.25;}\n"+

			".post.dev {position:relative; min-height:90px; border-bottom:1px solid #CCCCDD; padding-bottom:20px; padding-top:10px; clear:both;}\n"+
			".post.dev>div:first-child {display: inline-block; margin-right: 16px; border: none;}\n"+

			".wm.content > div > .toolBox {display:inline;}\n"+
			".wm.content > div > .toolBox > div {display:inline;}\n"+
			
			".post .toolBox {display:block; vertical-align:top; position:relative !important;}\n"+
			".post .toolBox > div {display:block; float:right;}\n"+
			
			"div.excluded {background-color:gray !important;}\n"+
			"div.working {background-color:yellow !important;}\n"+
			"div.timeout {background-color:orange !important;}\n"+
			"div.paused {background-color:silver !important;}\n"+
			"div.pinned {background-color:silver !important;}\n"+
			"div.nodef {background-color:deepskyblue !important;}\n"+
			"div.failed {background-color:red !important;}\n"+
			"div.accepted {background-color:limegreen !important;}\n"+
			"div.scam {background-color:purple !important;}\n"+
			
			".pausedHover {display:none; position:absolute; right:50%; top:50%;}\n"+
			".pausedHover>img {margin-left:-32px; margin-top:-32px;}\n"+
			".pausedHover>img:hover {background-color:rgba(0,255,0,0.5); border-radius:20%;}\n"+
			
			".post.paused.short>.floater>.pausedHover>img {background-color:rgba(0,255,0,0.5); border-radius:20%;}\n"+
			".post.paused>.pausedHover, .post.paused>.floater>.pausedHover {display:block;}\n"+
						
			".underline {border-bottom:1px solid #CCCCDD;}\n"+

			".toolTip {display:none; border:1px solid #767676; border-radius:3px; background-color:white; color:black; position:absolute; font-size:8pt; padding:5px; line-height: 12px; z-index:9999;}\n"+
			"*:hover > .toolTip {display:block;}\n"+
			".menuNode {width:0px; height:0px; position:absolute; background:none; border:none;top:-5px;}\n"+
			".toolTip.menuNode > ul {position:absolute; background-color: white; border: 1px solid; border-radius: 5px 5px 5px 5px; padding: 2px; min-width:100px;}\n"+
			".toolTip.menuNode > ul > li {position:relative; line-height:1.28; }\n"+
			".toolTip.right.menuNode {right:5px; }\n"+
			".toolTip.left.menuNode {left:-5px; }\n"+
			".toolTip.right.menuNode > ul {left:0px;}\n"+
			".toolTip.right.menuNode > ul > li {text-align:left;}\n"+
			".toolTip.left.menuNode > ul {right:0px;}\n"+
			".toolTip.left.menuNode > ul > li {text-align:right;}\n"+

			//little button div
			".littleButton {background-color:threedshadow; border-radius:5px; margin:1px; display:inline-block; vertical-align:middle;}\n"+
			".littleButton:hover {background-color:highlight !important;}\n"+
			".littleButton>img {position:relative; display:block; margin:2px;}\n"+
			".littleButton.oddOrange {background-color:#FF9968;}\n"+
			".littleButton.oddBlack {background-color:#82976E;}\n"+
			".littleButton.oddBlue {background-color:#51D1EA;}\n"+
			".littleButton.oddGreen {background-color:#B7E54F;}\n"+
			
			".menuEntry, .menuList > li {position:relative; border-radius:3px; border:1px solid white; padding:3px; min-width:100px;}\n"+
			".menuEntry:hover, .menuList > li:hover {border-color:#CCCCDD; background-color:#E0E8F6; }\n"+

			".accFailBlock {color: white !important;font-size: small !important;left: 16px;line-height: 12px;margin-bottom: -12px;padding: 0 !important;position: relative;top: -32px;}\n"+
			".accFailBlock .fail {background-color: #C3463A; border-radius: 2px 2px 2px 2px; box-shadow: 1px 1px 1px rgba(0, 39, 121, 0.77); padding: 1px 2px;}\n"+
			".accFailBlock .accept {background-color: #46B754; border-radius: 2px 2px 2px 2px; box-shadow: 1px 1px 1px rgba(0, 39, 121, 0.77); padding: 1px 2px;}\n"+

			//rules manager
			"#wmPriorityBuilder {margin:5px; position: relative; background-color:white; min-height:95%;}\n"+
			
			"#wmPriorityBuilder .validator > :before {content:'and: '}\n"+
			"#wmPriorityBuilder .validator:first-child > :before {content:'where: '}\n"+
			"#wmPriorityBuilder .action > :before {content:'and: '}\n"+
			"#wmPriorityBuilder .action:first-child > :before {content:'do: '}\n"+
			
			//collection feed node
			"#wmFeedNode {margin:5px; position: relative; background-color:white;}\n"+

			//sidekick manager
			"#wmSidekickList {margin:5px; position: relative; background-color:white; min-height:95%;}\n"+
			
			//feeds manager
			"#wmFeedsList {margin:5px; position: relative; background-color:white; min-height:95%;}\n"+

			//dynamic grabber
			"#wmDynamicBuilder {margin:5px; position: relative; background-color:white; min-height:95%;}\n"+
			
			//friend tracker
			"#wmFriendTracker {margin:5px; position: relative; background-color:white; min-height:95%;}\n"+

			".expanded {display:block;}\n"+
			".collapsed {display:none;}\n"+
						
			"label {font-weight:bold; margin-right:5px;}\n"+

			".unsaved {background-color:lightyellow !important;}\n"+

			".whiteover:hover {background-color:#FFFFFF !important;}\n"+
			".blueover:hover {background-color:#E0E8F6 !important;}\n"+

			".red {background-color:#C3463A !important; border: 2px solid #982B2F !important; text-shadow: -1px -1px 1px #982B2F, 1px 1px 1px #982B2F, 1px -1px 1px #982B2F, -1px 1px 1px #982B2F; text-transform: none !important; font-color:white !important;}\n"+
			".red:hover {background-color:#EA1515 !important;}\n"+

			".green {background-color:#46B754 !important; border: 2px solid #256E46 !important; text-shadow: -1px -1px 1px #256E46, 1px 1px 1px #256E46, 1px -1px 1px #256E46, -1px 1px 1px #256E46; text-transform: none !important; font-color:white !important;}\n"+
			".green:hover {background-color:#A6E11D !important;}\n"+

			".blue {background-color:#51C2FB !important; border: 2px solid #057499 !important; text-shadow: -1px -1px 1px #057499, 1px 1px 1px #057499, 1px -1px 1px #057499, -1px 1px 1px #057499; text-transform: none !important; font-color:white !important;}\n"+
			".blue:hover {background-color:#C2DEFF !important;}\n"+

			".gray {background-color:#999999 !important; border: 2px solid #666666 !important; text-shadow: -1px -1px 1px #666666, 1px 1px 1px #666666, 1px -1px 1px #666666, -1px 1px 1px #666666; text-transform: none !important; font-color:white !important;}\n"+
			".gray:hover {background-color:#C3C3C3 !important;}\n"+

			".odd {background-image: -moz-linear-gradient(center top , orange, red); box-shadow: 1px 1px 1px black; -moz-transform: rotate(15deg);}\n"+
			".odd:hover {-moz-transform: none;}\n"+

			".post.mosquito {width:16px; height:16px;}\n"+
			".post.tiny {width:24px; height:24px;}\n"+
			".post.small {width:32px; height:32px;}\n"+
			".post.medium {width:48px; height:48px;}\n"+
			".post.large {width:64px; height:64px;}\n"+
			".post.xlarge {width:96px; height:96px;}\n"+

			".floater.mosquito {left:8px;top:8px;}\n"+
			".floater.tiny {left:12px;top:12px;}\n"+
			".floater.small {left:16px;top:16px;}\n"+
			".floater.medium {left:24px;top:24px;}\n"+
			".floater.large {left:32px;top:32px;}\n"+
			".floater.xlarge {left:48px;top:48px;}\n"+

			".post.mosquito.working .picture img {width:24px; height:24px;left:-4px;top:-4px;}\n"+
			".post.tiny.working .picture img {width:32px; height:32px;left:-4px;top:-4px;}\n"+
			".post.small.working .picture img {width:48px; height:48px;left:-8px;top:-8px;}\n"+
			".post.medium.working .picture img {width:64px; height:64px;left:-8px;top:-8px;}\n"+
			".post.large.working .picture img {width:96px; height:96px;left:-16px;top:-16px;}\n"+
			".post.xlarge.working .picture img {width:128px; height:128px;left:-16px;top:-16px;}\n"+

			//"div.pinned {border-radius: 6px; background-color: black !important;}\n"+
			//".post.short.pinned .picture img {border-radius: 5px; height:80% !important; width:80% !important; margin-left:10%; margin-top:10%;}\n"+

			"#wmContent>.jsfTabControl>.tabs {top:10%; width:100px; position:relative;}\n"+
			"#wmContent>.jsfTabControl>.pages {border-radius:5px;}\n"+
			"#wmContent>.jsfTabControl>.tabs>.jsfTab {text-align:center;}\n"+
			
			".jsfTabControl>.tabs {font-family:impact; font-size:large; color:inactivecaptiontext;}\n"+
			"input,select,label,textarea {font-family:tahoma,arial; font-size:small; vertical-align:baseline !important;}\n"+
			".jsfComboBox {line-height:normal;}\n"+
			"button {font-family:tahoma,arial; font-size:small; vertical-align:top !important;}\n"+
			"input[type=\"checkbox\"] {font-family:tahoma,arial; font-size:small; vertical-align:middle !important;}\n"+

			".nomargin {margin:0 !important;}\n"+
			".hidden {display:none !important;}\n"+
			".block {display:block !important;}\n"+
			".alignTop {vertical-align:top !important;}\n"+
			".fit {width:100% !important;}\n"+
			".indent {margin-left:16px;}\n"+
			"img.crisp {image-rendering: -moz-crisp-edges;}\n"+
			
			".listItem {position:relative; clear:both;}\n"+
			".listItem.disabled {opacity:0.5 !important; background-color:#eeeeee;}\n"+
			".listItem .toolBox {position: absolute; right: 0px; top: 0;}\n"+
			".listItem select {border:0px; padding:0px; margin: 0px; margin-left:6px; margin-right:6px; background-color:#eeeeee; vertical-align:middle;}\n"+
			".listItem input {border:0px; padding:0px; margin: 0px; margin-left:6px; margin-right:6px; background-color:#eeeeee; vertical-align:middle;}\n"+
			".listItem textarea {background-color:#eeeeee; border:0px;}\n"+

			".header {background-color: window; font-family:impact; font-size:2em; color:inactivecaptiontext; border-radius:5px 5px 0 0; padding-left:6px;}\n"+
			".headerCaption {background-color: window; color: inactivecaptiontext; font-family: arial; font-size: small; padding-bottom: 6px; padding-left: 16px; padding-right: 16px; padding-top: 6px; border-radius:0 0 5px 5px;}\n"+
			".headerCaption+.toolBox, .header+.toolBox {border-bottom:1px solid activeborder; margin-bottom: 5px; margin-top: 5px; padding-bottom: 5px;}\n"+
			
			".line {border-top:1px solid #c0c0c0; line-height:2em;}\n"+
			".subsection {margin-left:28px;}\n"+
			
			".optioncontainer {max-height:12em; overflow-y:auto; background-color:rgb(238, 238, 238);}\n"+
			".optioncontainer>.line {line-height:normal;}\n"+
			
			".singleCol .post.classic {}\n"+
			".twoCol .post.classic {display: inline-block; width: 50%; vertical-align: top;}\n"+
			".twoCol .post.classic > .body {padding-right:28px;}\n"+
			".threeCol .post.classic {display: inline-block; width: 33%; vertical-align: top;}\n"+
			".threeCol .post.classic > .body {padding-right:28px;}\n"+
			".fourCol .post.classic {display: inline-block; width: 25%; vertical-align: top;}\n"+
			".fourCol .post.classic > .body {padding-right:28px;}\n"+

			".w400 {width:400px;}\n"+


			""
		}catch(e){log("WM.console.globalStyle: "+e);}},

		init: function(params){try{
			debug.print("WM.console.init:");
			var validateFBElements=["globalContainer","content"];
			params=params||{};

			//if console does not already exist
			if (!WM.console.tabContainer) {
				try{
					addGlobalStyle(WM.console.globalStyle(),"styleConsole");
				}catch(e){log("WM.console.init.addGlobalStyle: "+e);};

				//attach to facebook page
				var baseNode=$("globalContainer");
				if (baseNode) baseNode=baseNode.parentNode; 
				//or attach to page body
				else baseNode=($("body")||document.body);

				//sort fields shared by post sorting and grouping
				var sortFields = [
					{value:"age",title:"Time elasped since created (ms)."},
					{value:"alreadyProcessed",title:"History contains a status code for this post."},
					{value:"appID"},
					{value:"appName",title:"App name as it appears on FB."},
					{value:"date",title:"The datetime the post was created, in unix format. Does not contain millisecond data."},
					{value:"fromID",title:"The FB id of the person who created this post."},
					{value:"fromName",title:"The display name of the person who created this post."},
					{value:"fromNameLastFirst",title:"As fromName but displayed as LastName, FirstName"},
					{value:"id",title:"The post object id as it is connected with FB."},
					{value:"idText",title:"Either the whichText of the post, or the statusText of a post already processed."},
					{value:"isAccepted"},
					{value:"isFailed"},
					{value:"isTimeout"},
					{value:"isExcluded"},
					{value:"isCollect",title:"A flag for if this post is to be collected."},
					{value:"isForMe"},
					{value:"isLiked"},
					{value:"isMyPost"},
					{value:"isPaused"},
					{value:"isPinned"},
					{value:"isScam"},
					{value:"isStale"},
					{value:"isUndefined"},
					{value:"isWishlist"},
					{value:"isW2W",title:"If this post is a Wall-to-wall post or just a general feed post."},
					{value:"msg",title:"The comment attached to the post body by the post creator."},
					{value:"postedDay",title:"The year/month/day portion of the creation time for this post."},
					{value:"postedHour",title:"The year/month/day/hour portion of the creation time for this post."},
					{value:"priority",title:"Priority 0 being the first post you would want processed, and Priority 50 being default."},
					{value:"status",title:"The status code returned by the sidekick associated with this post."},
					{value:"which",title:"The sidekick-defined bonus type id for this kind of post."},
					{value:"whichText",title:"The text associated with this bonus type id."},
					{value:null,name:"(none)"},
				];

				//create our content window
				baseNode.insertBefore(createElement("div",{id:"wmContent"},[
					//toolbox
					(WM.console.tabContainer=new jsForms.tabControl({
						dock:"fillAndShare",
						sizeOffset:{height:-3,width:0},
						alignment:"left",
						tabs:[
							{ //collect tab
								text:"Collect",
								image:null,
								onSelect:function(){WM.console.collectTabControl.redraw();},
								content:[
									createElement("div",{className:"header",textContent:"Collect"}),
									createElement("div",{className:"headerCaption",textContent:"View friends' posts and manage all your collection needs."}),
									createElement("div",{className:"toolBox medium"},[
										createElement("span",{className:"littleButton oddBlue",title:"Fetch Newer Posts Now",onclick:function(){WM.fetch({newer:true,bypassPause:true});} },[createElement("img",{className:"resourceIcon rssUpRight24"})]),
										createElement("span",{className:"littleButton",title:"Fetch Older Posts Now",onclick:function(){WM.fetch({older:true,bypassPause:true});} },[createElement("img",{className:"resourceIcon rssDownLeft24"})]),
										WM.console.pauseFetchButton=createElement("span",{className:"littleButton oddOrange",title:"Pause Automatic Fetching",onclick:function(){WM.pauseFetching();} },[createElement("img",{className:"resourceIcon expandDown24"})]),
										WM.console.pauseCollectButton=createElement("span",{className:"littleButton oddOrange",title:"Pause Automatic Collection",onclick:function(){WM.pauseCollecting();} },[createElement("img",{className:"resourceIcon stop24"})]),
										createElement("span",{className:"littleButton",name:"0",title:"Classic View",onclick:WM.setDisplay},[createElement("img",{className:"resourceIcon layoutListColor24"})]),
										createElement("span",{className:"littleButton",name:"1",title:"Short View",onclick:WM.setDisplay},[createElement("img",{className:"resourceIcon layoutSmallColor24"})]),
										createElement("span",{className:"littleButton",name:"2",title:"Developer View",onclick:WM.setDisplay},[createElement("img",{className:"resourceIcon layoutDetailColor24"})]),
										createElement("span",{className:"littleButton",title:"Reset Counters",onclick:function(){WM.resetCounters();}},[createElement("img",{className:"resourceIcon refresh24"})]),
										createElement("span",{className:"littleButton oddOrange",title:"Clean Now",onclick:function(){WM.cleanPosts();}},[createElement("img",{className:"resourceIcon trash24"})]),
										createElement("span",{className:"littleButton",title:"ReID All",onclick:function(){WM.reIDAll();}},[createElement("img",{className:"resourceIcon identify24"})]),
										
										createElement("label",{className:"indent",textContent:"Sort By: "}),
										createElement("select",{id:"wmSortBy",className:"", title:"Sort By:", onchange:function(){WM.sortPosts({by:this.value});WM.redrawPosts({postRedraw:false,reorder:true});} },(function(){
											var ret=[];
											for (var i=0;i<sortFields.length;i++) ret.push(createElement("option",{value:sortFields[i].value,title:sortFields[i].title||"",textContent:sortFields[i].name||sortFields[i].value}));
											return ret;
										})()),
										createElement("span",{className:"littleButton oddGreen",title:"Sort Ascending",onclick:function(){WM.sortPosts({direction:"asc"});WM.redrawPosts({reorder:true, postRedraw:false});}},[createElement("img",{className:"resourceIcon sortAsc24"})]),
										createElement("span",{className:"littleButton oddOrange",title:"Sort Descending",onclick:function(){WM.sortPosts({direction:"desc"});WM.redrawPosts({reorder:true, postRedraw:false});}},[createElement("img",{className:"resourceIcon sortDesc24"})]),
										
										createElement("label",{className:"indent",textContent:"Group By: "}),
										createElement("select",{id:"wmGroupBy",className:"", title:"Group By:", onchange:function(){WM.constructGroups({by:this.value});WM.redrawPosts({postRedraw:false,reorder:true});} },(function(){
											var ret=[];
											for (var i=0;i<sortFields.length;i++) ret.push(createElement("option",{value:sortFields[i].value,title:sortFields[i].title||"",textContent:sortFields[i].name||sortFields[i].value}));
											return ret;
										})()),
										createElement("span",{className:"littleButton oddGreen",title:"Group Ascending",onclick:function(){WM.sortGroups({direction:"asc"});WM.redrawPosts({reorder:true, postRedraw:false});}},[createElement("img",{className:"resourceIcon sortAsc24"})]),
										createElement("span",{className:"littleButton oddOrange",title:"Group Descending",onclick:function(){WM.sortGroups({direction:"desc"});WM.redrawPosts({reorder:true, postRedraw:false});}},[createElement("img",{className:"resourceIcon sortDesc24"})]),

										createElement("label",{className:"indent",textContent:"Columns: ",title:"Classic Mode Only"}),
										createElement("select",{title:"Cols:", onchange:function(){WM.setDisplayCols({cols:this.value});} },[
											createElement("option",{value:1,textContent:"One",selected:WM.quickOpts.displayCols==1}),
											createElement("option",{value:2,textContent:"Two",selected:WM.quickOpts.displayCols==2}),
											createElement("option",{value:3,textContent:"Three",selected:WM.quickOpts.displayCols==3}),
											createElement("option",{value:4,textContent:"Four",selected:WM.quickOpts.displayCols==4})
										]),
										createElement("span",{className:"littleButton oddBlue",title:"Autolike Queue"},[
											createElement("img",{className:"resourceIcon like24"}),
											createElement("div",{className:"accFailBlock"},[
												WM.console.likeQueueCounterNode=createElement("span",{className:"accept",textContent:"0"})
											])											
										]),
									]),
									//app filter tabs
									(WM.console.collectTabControl=new jsForms.tabControl({
										dock:"fillAndShare",
										subStyle:"coolBar",
										shareSinglePage:true,
										preventAutoSelectTab:true,
										tabs:[
											{
												//default show all tab
												text:"Show ALL",
												image:"",
												imageClass:"resourceIcon allSidekicks32",
												appFilter:"All",
												onSelect:WM.setAppFilter,
												selected:(WM.quickOpts.filterApp=="All"),
												content:null, //because page is shared
											}
										],
										sharedContent:[
											//bonus display node
											WM.console.feedNode=createElement("div",{id:"wmFeedNode",style:"position: relative;"}),
										],
									})).node,
								],
							},
							{ //sidekicks tab
								text:"Manage Sidekicks",
								image:null,
								content:[
									createElement("div",{className:"header",textContent:"Manage Sidekicks"}),
									createElement("div",{className:"headerCaption",textContent:"Control some of the features of sidekicks."}),
									WM.console.sidekickNode=createElement("div",{id:"wmSidekickList",className:"scrollY"}),
								],
							},
							{ //feeds tab
								text:"Manage Feeds",
								image:null,
								content:[
									createElement("div",{className:"header",textContent:"Manage Feeds"}),
									createElement("div",{className:"headerCaption",textContent:"Add direct links to friends or other public profiles, and fetch posts from those feeds faster."}),
									createElement("div",{className:"toolBox medium columnRight"},[
										createElement("div",{},[
											createElement("div",{className:"littleButton oddGreen",title:"Add Feed",onclick:WM.feedManager.newFeed},[createElement("img",{className:"resourceIcon plus24"})]),
										])
									]),
									WM.console.feedManagerNode=createElement("div",{id:"wmFeedsList",className:"scrollY"}),
								],
							},
							{ //rules tab
								text:"Manage Rules",
								image:null,
								content:[
									createElement("div",{className:"header",textContent:"Manage Rules"}),
									createElement("div",{className:"headerCaption",textContent:"Create rules like macros to control exactly how posts are handled."}),
									createElement("div",{className:"toolBox medium columnRight"},[
										createElement("div",{},[
											createElement("div",{className:"littleButton oddGreen",title:"Add Rule",onclick:WM.rulesManager.newRule},[createElement("img",{className:"resourceIcon plus24"})]),
											createElement("div",{className:"littleButton oddBlue",title:"Reset All Limits",onclick:WM.rulesManager.resetAllLimits},[createElement("img",{className:"resourceIcon reset24"})]),
											createElement("div",{className:"littleButton oddBlue",title:"Convert Dynamics",onclick:WM.rulesManager.convertDynamics},[createElement("img",{className:"resourceIcon exportGrab24"})]),
											createElement("div",{className:"littleButton oddBlue",title:"Import Rule",onclick:WM.rulesManager.importRule},[createElement("img",{className:"resourceIcon importData24"})]),
											createElement("div",{className:"littleButton oddBlue",title:"Export All Rules",onclick:WM.rulesManager.showData},[createElement("img",{className:"resourceIcon object24"})]),
											WM.rulesManager.toggleHBNode=createElement("div",{className:"littleButton "+(WM.quickOpts.heartbeatDisabled?"oddOrange":"oddGreen"),title:"Toggle Heartbeat",onclick:WM.rulesManager.toggleHeartbeat},[createElement("img",{className:"resourceIcon heartbeat24"})]),
										])
									]),
									WM.console.priorityBuild=createElement("div",{id:"wmPriorityBuilder",className:"scrollY"}),
								],
							},
							{ //dynamics tab
								text:"Dynamic Grabber",
								image:null,
								content:[
									createElement("div",{className:"header",textContent:"Dynamic Grabber"}),
									createElement("div",{className:"headerCaption",textContent:"Create tests to capture posts sidekicks might not."}),
									createElement("div",{className:"toolBox medium columnRight"},[
										createElement("div",{},[
											createElement("div",{className:"littleButton oddGreen",title:"Add Test",onclick:WM.grabber.newTest},[createElement("img",{className:"resourceIcon plus24"})]),
											createElement("div",{className:"littleButton oddBlue",title:"Import Test",onclick:WM.grabber.importTest},[createElement("img",{className:"resourceIcon importData24"})]),
										])
									]),
									WM.console.dynamicBuild=createElement("div",{id:"wmDynamicBuilder",className:"scrollY"}),
								],
							},
							{ //friends tab
								text:"Friend Tracker",
								image:null,
								content:[
									createElement("div",{className:"header",textContent:"Friend Tracker"}),
									createElement("div",{className:"headerCaption",textContent:"Track player friends and your interactions with them."}),
									createElement("div",{className:"toolBox medium columnRight"},[
										createElement("span",{className:"littleButton oddOrange",title:"Clean Now",onclick:function(){WM.friendTracker.clearAll();}},[
											createElement("img",{className:"resourceIcon trash24"})
										]),
										
										createElement("label",{className:"indent",textContent:"Sort By: "}),
										createElement("select",{title:"Sort By:", onchange:function(){WM.friendTracker.sort({sortBy:this.value});} },[
											createElement("option",{value:"acceptCount",textContent:"acceptCount",title:"How many posts WM remembers as collected successfully from this user."}),
											createElement("option",{value:"failCount",textContent:"failCount",title:"How many posts WM remembers as failed from this user."}),
											createElement("option",{value:"id",textContent:"id",title:"The facebook id of the user."}),
											createElement("option",{value:"lastKnownPostDate",textContent:"lastKnownPostDate",title:"The date of the last known post WM received for this user."}),
											createElement("option",{value:"name",textContent:"name",title:"The name of the user, with last name first."}),
											createElement("option",{value:"postCount",textContent:"postCount",title:"How many posts WM remembers receiving related to this user."}),
											createElement("option",{value:"totalCount",textContent:"totalCount",title:"How many posts WM remembers failed OR accepted from this user."})
										]),
										createElement("span",{className:"littleButton oddGreen",title:"Sort Ascending",onclick:function(){WM.friendTracker.sort({sortOrder:"asc"});}},[createElement("img",{className:"resourceIcon sortAsc24"})]),
										createElement("span",{className:"littleButton oddOrange",title:"Sort Descending",onclick:function(){WM.friendTracker.sort({sortOrder:"desc"});}},[createElement("img",{className:"resourceIcon sortDesc24"})]),
									]),
									WM.console.friendBuild=createElement("div",{id:"wmFriendTracker",className:"scrollY"}),
								],
							},
							{ //options tab
								text:"Options",
								image:null,
								content:[
									createElement("div",{className:"header",textContent:"Options"}),
									createElement("div",{className:"headerCaption",textContent:"Manage script and sidekick configuration, or link to updates."}),
									//config menu button
									createElement("div",{},[
										createElement("label",{textContent:"Open the options menu: "}),
										WM.console.configButton=createElement("button",{
											className:"jsfHidden",
											textContent:"WM Options",											
											onclick:function(){
												//open options menu
												WM.config.open();
											},
										}),
									]),
									//update script button
									createElement("div",{},[
										createElement("label",{textContent:"Update Script (Current Version: "+WM.version+") :"}),
										createElement("button",{
											className:"",
											textContent:"Update Script",
											onclick:function(){
												//open update url in new window/tab
												window.open("http://userscripts.org/scripts/source/86674.user.js","_blank");
											},
										}),
									]),									
								],
							},
						]
					})).node
				]),  $("globalContainer"));

				//destroy facebook content on page
				if ($("content")) $("content").style.display="none !important";
				
				//init sort order
				$("wmSortBy").value=WM.quickOpts.sortBy;

				//init group order
				$("wmGroupBy").value=WM.quickOpts.groupBy;

				//init display mode
				with (WM.console.feedNode){
					className = className.toggleWordB(["1","3"].inArray(WM.quickOpts.displayMode),"short");						
				}
				WM.setDisplayCols({cols:WM.quickOpts.displayCols});				
			}
			WM.console.initialized = true;

			//give sidekicks time to dock
			if (params["callback"]||null) {
				var fx = params["callback"];
				delete params["callback"];
				doAction(fx);
			}
			
		}catch(e){log("WM.console.init: "+e);}},
		
	}; //end WM.console

//***************************************************************************************************************************************
//***** Sidekick Docking Object
//***************************************************************************************************************************************
	WM.dock = {
		//restructure menu to append appID before every object
		fixMenu: function(menu,app){try{
			var ret={};
			//for each object in menu
			for (var o in menu){
				//WM.message(o);
				ret[app+o]=menu[o];

				//fix button functions and arrays to be prepended by the appID of that sidekick
				var t=menu[o]["type"];
				switch(t){
					case "button_highlight":
					case "button_selectmulti":
					case "button_selectprefix":

						//fix elements in the clearfirst array
						if (menu[o]["clearfirst"]){
							for (var i=0,len=ret[app+o]["clearfirst"].length;i<len;i++){
								ret[app+o]["clearfirst"][i] = app+ret[app+o]["clearfirst"][i];
							}
						}

						//fix elements in the options array
						if (menu[o]["options"]){
							for (var i=0,len=ret[app+o]["options"].length;i<len;i++){
								ret[app+o]["options"][i] = app+ret[app+o]["options"][i];
							}
						}

						if (menu[o]["clearPrefix"]){
							ret[app+o]["clearPrefix"]=app+ret[app+o]["clearPrefix"];
						}

						if (menu[o]["prefix"]){
							ret[app+o]["prefix"]=app+ret[app+o]["prefix"];
						}
				}

				//fix kids
				if (menu[o]["kids"]){
					//rebuild kids object
					ret[app+o]["kids"]=WM.dock.fixMenu(menu[o]["kids"],app);
				}
			}
			return ret;
		} catch(e) {log("WM.dock.fixMenu: "+e);}},

		//restructure tests to append appID before every object's return
		fixTests: function(arr,app){try{
			//for each test in array
			for (var t=0,len=arr.length;t<len;t++) {
				var ret=arr[t].ret, kids=arr[t].kids;
				//replace return value
				if (ret) {
					if (ret!="exclude" && ret!="none") {
						arr[t].ret=app.appID+ret;
					}
				}
				//process subtests
				if (kids) WM.dock.fixTests(kids,app);
			}
		} catch(e) {log("WM.dock.fixTests: "+e);}},

		fixAcceptTexts:function(app){try{
			var newAccText={};
			for (var s in app.accText) {
				newAccText[app.appID+s]=app.accText[s];
			}
			app.accText=newAccText;
		} catch(e) {log("WM.dock.fixAcceptTexts: "+e);}},

		onSidekickParsed: function(newset){try{		
			//save it into the NEW format for games
			var app=(WM.apps[newset.appID]=new WM.App(newset));

			//promptText JSON.stringify(WM.config.settings));
			WM.updateSettingsValues();

			//detach the menu from the newset to reduce duplication
			delete app.menu;
			
			//fire priority event
			(function(){WM.rulesManager.doEvent("onSidekickDock",app);})();
			
			//fetch its initial posts
			//app.fetchPosts();
			WM.newSidekicks.push(app);
			
		}catch(e){log("WM.dock.onSidekickParsed: "+e);}},
		
		parseNewSidekick: function(node){try{
			if (node){	
				var v = node.getAttribute('data-ft');
				node.setAttribute('data-ft','');
				if (v||null) {
					var newset = JSON.parse(v);
					WM.dock.onSidekickParsed(newset);
				}
			}
		} catch(e) {log("WM.dock.parseNewSidekick: "+e);}},
		
		answerDockingDoor: function(){try{
			//log("Sidekick requesting to dock");

			//get all sidekicks that left info on the dock;
			forNodes(".//div[@id='wmDock']/div[(@data-ft) and not(@data-ft='')]",{},function(node){
				if (node.getAttribute('data-ft') !=""){
					window.setTimeout(WM.dock.parseNewSidekick,1,node);
				}
			});
		} catch(e) {log("WM.dock.answerDockingDoor: "+e);}},

	};
	
//***************************************************************************************************************************************
//***** Collector Object
//***************************************************************************************************************************************
	WM.collector = {
		tabs : {}, //container for window objects
		recycle : [], //container for reusable window objects
		queue : [], //container for urls to do in order
		count : 0,

		windowExists : function(hwnd){
			try{
				var testUrl=tab.hwnd.location.toString();
				return true;
			}catch(e) {
				return false;
			}
		},
		
		//requires id, url and callback
		open : function(params) {try{
			//log("WM.collector.open()",{level:0});

			//check for tab queueing
			if (WM.opts.queuetabs && WM.collector.count && !(params.emergency||false)) {
				if (params.first||false) {
					//cut in line to be next processed
					WM.collector.queue.unshift(params);
					return;
				}
				//toss the next action in the queue while we wait for the current one to finish
				WM.collector.queue.push(params);
				//log("WM.collector.open: request queued",{level:1});
				return;
			}

			var url = params.url;
			var id = params.id;

			//create a window or use a recycled one
			var tabHwnd;
			if (WM.collector.recycle.length) {
				tabHwnd = WM.collector.recycle.shift();
				//watch for missing window objects
				try{
					//use the existing window object if it responds
					tabHwnd.location.href=url;
				} catch (e) {
					//window object missing, make a new one
					//FF22 version
					tabHwnd = GM_openInTab(url,"_blank");
					//FF21 version
					//tabHwnd = ((WM.opts.useGM_openInTab)?GM_openInTab:window.open)(url,"_blank");
				}
			} else {
				//we do not use recycling, just make a new one
				//FF22 version
				tabHwnd = GM_openInTab(url,"_blank");
				//FF21 version
				//tabHwnd = ((WM.opts.useGM_openInTab)?GM_openInTab:window.open)(url,"_blank");
			}

			//window opening
			if (tabHwnd) {
				WM.collector.count++;
				params.hwnd=tabHwnd; //store the window handle
				params.openTime=timeStamp();
				WM.collector.tabs[id]=params; //add the tab and all its data to the array

				//pass data to the sidekick top window
				var callback = params.callback;
				if (callback) delete params.callback;
				if (params.msg) {
					remove($(params.msg));
					delete(params.msg);
				}
				
				//details for posts, not for likes
				var app, synApp, isPost;
				if (isPost=(params.post||null)){
					app=params.post.app; 
					synApp=app.parent||app;
				}
				

				if (callback) {
					//log("WM.collector.open: callback fired",{level:3});
					doAction(function(){
						callback(params);
					});
				}
			} else {
				log("WM.collector: Tab or Window is not opening or your browser does not support controlling tabs and windows via scripts. Check your popup blocker.",{level:5});
			}
		}catch(e){log("WM.collector.open: "+e);}},

		doNext : function(){try{WM.collector.open(WM.collector.queue.shift());}catch(e){log("WM.collector.doNext: "+e);}},

		close : function(tab) {try{
			//recycle or close the passed tab
			try{
				if (WM.opts.recycletabsall || WM.opts.queuetabs || (WM.collector.recycle.length < WM.opts.recycletabs)) {
					//wipe it and put it away
					
					if (tab.hwnd){
						WM.collector.recycle.push(tab.hwnd);
						tab.hwnd.location.href="about:blank";
						if (WM.collector.windowExists(tab.hwnd)){
							tab.hwnd.location.hash="";
						}
					} else {
						//tab is busy, laggy or missing
						tab.closeRetries=(tab.closeRetries||0)+1;
						if (tab.closeRetries<3) {
							setTimeout(function(){WM.collector.close(tab);},1000);
						} else {
							log("WM.collector.close: Control of window handle lost; cannot recycle. Window may be too busy to communicate with, or has been closed manually.");
						}
						return;
					}
				} else {
					if (tab.hwnd) tab.hwnd.close();
				}
			} catch (e){log("WM.collector.close: recycler: "+e);}

			try{
				tab.hwnd=null;
				delete tab.signal;
				delete tab.stage;
				delete tab.closeTries;
			
				if (tab.toIntv) clearInterval(tab.toIntv);
				delete tab; 
				tab=null;
				WM.collector.count--
			}catch(e){log("WM.collector.close: destroy tab: "+e);}

			//check for items in queue to do next
			if (WM.collector.queue.length) {
				//check that queueing is still in practice
				if (WM.opts.queuetabs) {
					setTimeout(WM.collector.doNext,1000); //just do one
				} else {
					//options have changed since queueing was enacted, release all the queue into windows right now
					var offset=1000;
					while (WM.collector.queue.length && (WM.collector.count < WM.opts.maxrequests)) {
						setTimeout(WM.collector.doNext,offset); //open all, up to the limit set in options
						offset+=100;
					}
				}
			}

		} catch (e){log("WM.collector.close: "+e);}},

		closeAll : function() {try{
			//first delete the queue so close fx doesnt pick them up
			WM.collector.queue=[]; //empty but dont destroy

			//then close the active windows, moving any to the recycler if that is enabled
			for (var t in WM.collector.tabs) {
				WM.collector.close(WM.collector.tabs[t]);
			}

			//then close any recycled windows
			if (WM.collector.recycle.length) {
				for (var r=0, hwnd; r < WM.collector.recycle.length; r++) {
					if (hwnd=WM.collector.recycle[r]) {
						hwnd.close();
					}
				}
				WM.collector.recycle=[];
			}
		} catch (e){log("WM.collector.closeAll: "+e);}},
		
		createTimer : function(tab) {try{
			//create a timeout handler based on options and store the timer on the tab
			tab.toIntv=setTimeout(function(){
				if (tab) if (tab.stage!=4) doAction(function(){
					//tab has been active too long, do timeout
					log("WM.collector.timer: request timeout ("+tab.id+")",{level:3});
					WM.setAsFailed(null, -14, tab.post);
					WM.clearURL(tab);
				})
			},WM.opts.reqtimeout*1000);
		} catch (e){
			log("WM.collector.createTimer: "+e);}
		},

		cancelProcess : function(params) {try{
			params=params||{};
			var c = WM.collector;
			
			for (t in c.tabs) {
				if (c.tabs[t] && c.tabs[t][params.search] && c.tabs[t][params.search]==params.find){
					//matching collector tab found
					tab=c.tabs[t];
					
					//close the window
					c.close(tab);
				}
			}
		} catch (e){log("WM.collector.cancelProcess: "+e);}},
		
		refreshProcess : function(params) {try{
			params=params||{};
			var c = WM.collector;
			for (t in c.tabs) {
				if (c.tabs[t] && c.tabs[t][params.search] && c.tabs[t][params.search]==params.find){
					//matching collector tab found
					tab=c.tabs[t];
					
					//restart the window at its initial url
					if (tab.hwnd.location.href==tab.url) {
						tab.hwnd.location.reload();
					} else {
						tab.hwnd.location.href=tab.url;
					}
				}
			}
		} catch (e){log("WM.collector.refreshProcess: "+e);}},

	};

//***************************************************************************************************************************************
//***** Dynamic Grabberrabber Object
//***************************************************************************************************************************************
	WM.grabber = {
		tests:[],

		methods:["msg","fromID","fromName","url","body","html","targetID","targetName","caption","title","desc","comments",
				"commentorID","commentorName","likeName","likeID","link","either","img","canvas"],

		init:function(params){try{
			params=(params||{});
			var testsIn = getOptJSON("dynamics_"+WM.currentUser.profile) || [];
			var globalsIn = getOptJSON("dynamics_global") || {};
			//import locals and intermix globals we have a placeholder for
			if (isArrayAndNotEmpty(testsIn)) {
				for (var t=0; t<testsIn.length; t++) {
					if (testsIn[t].isGlobal) {
						//make sure the global test still exists
						var glob=globalsIn[testsIn[t].uniqueID]||null;
						if (glob){
							//merge global and local data
							//this retains our expanded/enabled parts
							var merge=mergeJSON(glob, testsIn[t]);
							WM.grabber.newTest(merge);
							//flag it so we don't import it again below
							glob.alreadyUsed=true;
						} else {
							//global missing, can't import
							log("WM.grabber.init: Global test missing, cannot merge");
						}
					} else {
						//load from locals
						WM.grabber.newTest(testsIn[t]);
					}
				}
			}
			//import all globals not already accounted for
			for (var t in globalsIn) {
				var glob=globalsIn[t];
				//avoid already imported globals
				if (!glob.alreadyUsed){
					glob.uniqueID=t;
					glob.isGlobal=true;
					WM.grabber.newTest(glob);
				}
			}
			
		}catch(e){log("WM.grabber.init: "+e);}},

		save:function(){try{
			var ret=[];
			var retGlobal={};
			
			if (isArrayAndNotEmpty(WM.grabber.tests)) {
				for (var t=0, len=WM.grabber.tests.length; t<len; t++){
					var test=WM.grabber.tests[t];
					if (!test.isGlobal) {
						//save it locally
						ret.push(test.saveableData);
					} else {
						//make a placeholder locally
						ret.push({isGlobal:true, uniqueID:test.uniqueID, enabled:test.enabled, expanded:test.expanded});
						//and save it globally
						var glob=test.saveableData;
						glob.uniqueID=test.uniqueID;
						retGlobal[test.uniqueID]=glob;
					}
				}
			}
			setOptJSON("dynamics_"+WM.currentUser.profile,ret);
			setOptJSON("dynamics_global",retGlobal);
		}catch(e){log("WM.grabber.save: "+e);}},

		newTest:function(params){try{
			params=params||{};
			var test = new WM.Test(params);
			WM.grabber.tests.push(test);
			WM.grabber.save();
		}catch(e){log("WM.grabber.newTest: "+e);}},

		importTest:function(){try{
			var params=prompt("Input test data",null);
			if (params) {
				WM.grabber.newTest(JSON.parse(params));
			}
		}catch(e){log("WM.grabber.importTest: "+e);}},

		//get the test object with id starting at optional node or at top level
		//may return null
		getTest:function(id,node){try{
			var nodes=(node||WM.grabber.tests);
			for (var i=0,len=nodes.length;i<len;i++){
				if (nodes[i]["id"]==id) {
					return nodes[i];
				} else if (nodes[i]["kids"]) {
					var ret = WM.grabber.getTest(id,nodes[i]["kids"]);
					if (ret) return ret;
				}
			}
		}catch(e){log("WM.grabber.getTest: "+e);}},
	};
	
//***************************************************************************************************************************************
//***** Test Class
//***************************************************************************************************************************************
	WM.Test = function(params){try{
		this.objType="test";
		var self=this;
		params=params||{};
		
		//defaults
		this.enabled=!(params.disabled||false); //check for WM2 disabled param
		this.expanded=true;
		this.title="";
		this.search=[]; //strings array
		this.find=""; //string
		this.findArray=[]; //string array
		this.kids=[]; //test array
		this.subTests=[]; //strings array
		this.parent=null;
		this.appID="";
		this.ret="dynamic";
		this._findMode="basic";
		this.subNumRange={low:0,high:0};
		this._isGlobal=false;
		
		this.__defineGetter__("saveableData",function(){try{
			var dat={};
				
			//dat.id=this.id;
			dat.label=this.title;
			dat.enabled=this.enabled;
			dat.search=this.search;
			dat.find=(this.findMode=="basic")?this.findArray:this.find;
			dat.ret=this.ret;
			dat.expanded=this.expanded;
			if (this.findMode=="subtests") dat.subTests=this.subTests;
			if (this.findMode=="subnumrange") {
				dat.subNumRange=this.subNumRange.low+","+this.subNumRange.high;
			}
			if (this.findMode=="regex") dat.regex=this.regex;
			dat.appID=this.appID;
			dat.kids=[];
			if (isArrayAndNotEmpty(this.kids)) for (var i=0,kid;(kid=this.kids[i]);i++) {
				dat.kids.push(kid.saveableData);
			}
			return dat;
		}catch(e){log("WM.Test.saveableData: "+e);}});
		
		//set/get wether this test is saved as global or profile
		this.__defineGetter__("isGlobal",function(){try{
			return this._isGlobal;
		}catch(e){log("WM.Test.isGlobal: "+e);}});
		this.__defineSetter__("isGlobal",function(v){try{
			//only top level tests can be global
			if (this.parent) {
				confirm("Only top level tests can be set to global.");
				return;
			} 
			
			if (!v) {
				if (!confirm("Disabling profile sharing on this test will prevent other users on this machine from loading it. Are you sure you wish to make this test locally available only?")) return;
			}
		
			this._isGlobal=v;
			
			//make sure we have a uniqueID
			//but don't destroy one that already exists
			if (v && !exists(this.uniqueID)) this.uniqueID = unique();
			
			//change the color/icon of the isGlobal button
			if (this.toggleGlobalButton) {
				var s=WM.opts.littleButtonSize;
				with (this.toggleGlobalButton) className=className.swapWordB(v,"removeGlobal"+s,"addGlobal"+s);
				with (this.toggleGlobalButton.parentNode) {
					className=className.swapWordB(v,"oddOrange","oddGreen");
					title=(v)?"Disable Profile Sharing":"Share With Other Profiles";
				}
			}
		}catch(e){log("WM.Test.isGlobal: "+e);}});
		
		//use passed params
		for (var p in params) {
			//omit specific params
			if (!(["subNumRange","kids","disabled","label","find"].inArray(p)) ) {
				//copy only params that make it past the checker
				this[p]=params[p];
			}
		}
		
		//calculate subNumRange as an object
		if (exists(params.subNumRange)) {
			var p=params.subNumRange.split(",");
			this.subNumRange={low:p[0]||0, high:p[1]||0};
			this._findMode="subnumrange";
		}
		
		//get the title from the label field
		if (exists(params.label)) this.title=params.label;
		
		//detect which findMode we are using
		//subNumRange was already inspected above
		if (this.regex) this._findMode="regex";
		else if (exists(params.subTests)) this._findMode="subtests";
		//and we default to "basic" already
		
		//import the find field now
		if (isArray(params.find)) this.findArray=params.find;
		else this.find=params.find;
				
		this.enable=function(){try{
			this.enabled=true;
			this.node.className=this.node.className.removeWord("disabled");
			WM.grabber.save();
		}catch(e){log("WM.Test.enable: "+e);}};

		this.disable=function(){try{
			this.enabled=false;
			this.node.className=this.node.className.addWord("disabled");
			WM.grabber.save();
		}catch(e){log("WM.Test.disable: "+e);}};

		this.remove=function(noConfirm){try{
			var ask=WM.opts.dynamicConfirmDeleteTest;
			if (noConfirm || (this.isGlobal && confirm("This test is shared with other profiles. Deleting it here will prevent it from loading for other users. Are you sure you wish to delete this test and its children.")) || !ask || (!this.isGlobal && ask && confirm("Delete test and all of its child nodes?"))){
				//remove my data
				var parentContainer=(this.parent)?this.parent.kids:WM.grabber.tests;
				parentContainer.removeByValue(this);
				//remove my node
				remove(this.node);
				
				doAction(WM.grabber.save);
			}
		}catch(e){log("WM.Test.remove: "+e);}};

		this.moveUp=function(){try{
			//where is this
			var parentContainer=(this.parent)?this.parent.kids:WM.grabber.tests;
			//only affects items not already the first in the list
			//and not the only child in the list
			if ((parentContainer.length>1) && (parentContainer[0]!=this)) {
				//which index is this?
				var myIndex=parentContainer.inArrayWhere(this);
				if (myIndex != -1) {
					//I have a proper index here
					//who is my sibling
					var sibling = parentContainer[myIndex-1];
					//swap me with my sibling
					parentContainer[myIndex-1]=this;
					parentContainer[myIndex]=sibling;
					//place my node before my sibling node
					sibling.node.parentNode.insertBefore(this.node,sibling.node);
					//save it
					WM.grabber.save();
				}
			}
		}catch(e){log("WM.Test.moveUp: "+e);}};
		
		this.moveDown=function(){try{
			//where is this
			var parentContainer=(this.parent)?this.parent.kids:WM.grabber.tests;
			//only affects items not already the last in the list
			//and not the only child in the list
			if ((parentContainer.length>1) && (parentContainer.last()!=this)) {
				//which index is this?
				var myIndex=parentContainer.inArrayWhere(this);
				if (myIndex != -1) {
					//I have a proper index here
					//who is my sibling
					var sibling = parentContainer[myIndex+1];
					//swap me with my sibling
					parentContainer[myIndex+1]=this;
					parentContainer[myIndex]=sibling;
					//place my node before my sibling node
					sibling.node.parentNode.insertBefore(sibling.node,this.node);
					//save it
					WM.grabber.save();
				}
			}
		}catch(e){log("WM.Test.moveDown: "+e);}};

		this.moveUpLevel=function(){try{
			if (this.parent) {
				//this is not a top level node, so we can move it
				var targetContainer=((this.parent.parent)?this.parent.parent.kids:WM.grabber.tests);
				//remove from parent
				this.parent.kids.removeByValue(this);
				//set new parent
				this.parent=(this.parent.parent||null); //never point to the top level
				//move the object
				targetContainer.push(this);
				//move the node
				if (this.parent) this.parent.kidsNode.appendChild(this.node);
				else WM.console.dynamicBuild.appendChild(this.node);
				
				//save it
				WM.grabber.save();
			}
		}catch(e){log("WM.Test.moveUpLevel: "+e);}};
		
		this.moveDownLevel=function(){try{
			//where is this
			var parentContainer=(this.parent)?this.parent.kids:WM.grabber.tests;
			//create a new rule at my level
			var newTest = new WM.Test({
				parent:this.parent||null,
			});
			parentContainer.push(newTest);
			//remove me from my current parent
			parentContainer.removeByValue(this);
			//attach me to my new parent
			this.parent=newTest;
			newTest.kids.push(this);
			//move my node
			newTest.kidsNode.appendChild(this.node);
			//save it
			WM.grabber.save();
		}catch(e){log("WM.Test.moveDownLevel: "+e);}};
		
		this.clone=function(){try{
			var cloneTest=this.saveableData;
			//global clones are not global
			if (this.parent) this.parent.addChild(cloneTest);
			else WM.grabber.newTest(cloneTest);
		}catch(e){log("WM.Test.clone: "+e);}};

		this.addChild=function(p){try{
			var isNew=!exists(p);
			p=p||{};
			p.parent=this;
			var test=new WM.Test(p);
			this.kids.push(test);
			if (isNew) WM.grabber.save();
		}catch(e){log("WM.Test.addChild: "+e);}};

		this.toggleContent=function(){try{
			this.expanded=!this.expanded;
			var btnSize=WM.opts.littleButtonSize;
			with (this.contentNode)
				className=className.swapWordB(this.expanded,"expanded","collapsed");
			with (this.toggleImgNode)
				className=className.swapWordB(this.expanded,"treeCollapse"+btnSize,"treeExpand"+btnSize);
			WM.grabber.save();
		}catch(e){log("WM.Test.toggleContent: "+e);}};
		
		this.populateBonusList=function(){try{
			var node=this.bonusNode;
			var bonuses={};
			//get the list of accept texts for this app
			if (this.appID!="") {
				if (this.appID=="*") {
					//populate list with bonuses from ALL docked sidekicks
				} else {
					//make sure the app is ready
					//if it has not yet docked, it wont be
					var app=WM.apps[this.appID];
					bonuses = (app?(mergeJSON(app.accText,app.userDefinedTypes)||{}):{});
				}
			}			
			//add special return values
			bonuses["dynamic"]="* Dynamic grab";
			bonuses["none"]="* None";
			bonuses["wishlist"]="* Flaged as Wishlist";
			bonuses["exclude"]="* Excluded types";
			bonuses["send"]="* Send Unknown";
			bonuses["doUnknown"]="* Get Unknown";
			bonuses["{%1}"]="* Subtest Value";

			//sort by display text
			bonuses=sortCollection(bonuses,"value");
			
			//add each element to the dropdown
			var elem;
			node.innerHTML=""; //wipe previous list
			for (var i in bonuses) {
				var showI=i.removePrefix(this.appID);
				node.appendChild(
					elem=createElement("option",{textContent:((bonuses[i].startsWith("*"))?"":((showI.startsWith("send"))?"Send ":"Get "))+bonuses[i], value:i, selected:(this.ret==i)})
				);
			}

		}catch(e){log("WM.Test.populateBonusList: "+e);}};
		
		this.populateAppList=function(){try{
			var node=this.appListNode;
			var a={};
			for (var i in WM.apps){
				a[WM.apps[i].appID]=WM.apps[i].name;
			}

			//add special return values
			a["*"]="* All";

			//add each element to the dropdown
			var elem;
			node.innerHTML=""; //wipe previous list
			for (var i in a) {
				node.appendChild(elem=createElement("option",{textContent:a[i], value:i,selected:(this.appID==i)}));
			}

			//sort it
			elementSortChildren(node,"textContent");
		}catch(e){log("WM.Test.populateAppList: "+e);}};

		this.calcSearch=function(){try{
			//collect the checked search fields in their listed order
			if (self.searchNode) {
				self.search=[];
				forNodes(".//input[(@type='checkbox')]",{node:self.searchNode},function(e){
					if (e && e.checked){
						self.search.push(e.value);
						log(e.value);
					}
				});
			}
			WM.grabber.save();
		}catch(e){log("WM.Test.calcSearch: "+e);}};
		
		this.convertToRule=function(p){try{
			var rule;
			WM.rulesManager.rules.push( 
				rule=new WM.rulesManager.Rule( WM.rulesManager.ruleFromTest( this.saveableData ) ) 
			);
			if (WM.opts.rulesJumpToNewRule){
				//jump to rule view
				WM.console.tabContainer.selectTab(3);
				//scroll to new rule
				rule.node.scrollIntoView();
			}
		}catch(e){log("WM.Test.convertToRule: "+e);}};		
		
		//set/get find field modes
		this.__defineGetter__("findMode",function(){try{
			return this._findMode;
		}catch(e){log("WM.Test.findMode: "+e);}});
		this.__defineSetter__("findMode",function(v){try{
			var lastV = this._findMode;
			this._findMode=v;
			if (lastV==v) return; //no change
			
			//enable disable regex type
			this.regex=(v=="regex" || v=="regexp");
			
			//switch to array/string find field type
			//this.setFindType((v=="basic")?"array":"string");
			
			//show the correct find field
			if (this.findNode) this.findNode.value=((v=="basic")?this.findArray.join("\n"):this.find);

			//show/hide the subtests box
			if (this.subTestsBoxNode) with (this.subTestsBoxNode) className=className.toggleWordB((v!="subtests"),"hidden");
			
			//show/hide the subnumrange picker
			if (this.subNumRangeBoxNode) with (this.subNumRangeBoxNode) className=className.toggleWordB((v!="subnumrange"),"hidden");
			
			WM.grabber.save();

		}catch(e){log("WM.Test.findMode: "+e);}});

		//draw it
		try{(((this.parent)?this.parent.kidsNode:null)||$("wmDynamicBuilder")).appendChild(
			this.node=createElement("div",{className:"listItem "+((this.enabled)?"enabled":"disabled")},[
				createElement("div",{className:"line"},[
					createElement("div",{className:"littleButton",title:"Toggle Content",onclick:function(){self.toggleContent();}},[
						this.toggleImgNode=createElement("img",{className:"resourceIcon "+(this.expanded?"treeCollapse"+WM.opts.littleButtonSize:"treeExpand"+WM.opts.littleButtonSize)}),
					]),
					this.toggleNode=createElement("input",{type:"checkbox",checked:this.enabled,onchange:function(){
						self.enabled=this.checked;
						with (self.node) className=className.toggleWordB(!this.checked,"disabled");
						WM.grabber.save();
					}}),
					createElement("label",{textContent:"Title:"}),
					this.titleNode=createElement("input",{value:(this.title||""), onchange:function(){self.title=this.value; WM.grabber.save();}}),
					
					//toolbox
					createElement("div",{className:"littleButton oddOrange", title:"Remove Test"},[
						createElement("img",{className:"resourceIcon trash"+WM.opts.littleButtonSize,onclick:function(){self.remove();}})]),
					createElement("div",{className:"littleButton oddBlue", title:"Clone Test"},[
						createElement("img",{className:"resourceIcon clone"+WM.opts.littleButtonSize,onclick:function(){self.clone();}})]),
					createElement("div",{className:"littleButton oddGreen", title:"Move Up"},[
						createElement("img",{className:"resourceIcon arrowUp"+WM.opts.littleButtonSize,onclick:function(){self.moveUp();}})]),
					createElement("div",{className:"littleButton oddOrange", title:"Move Down"},[
						createElement("img",{className:"resourceIcon arrowDown"+WM.opts.littleButtonSize,onclick:function(){self.moveDown();}})]),
					createElement("div",{className:"littleButton oddGreen", title:"Move Up Level"},[
						createElement("img",{className:"resourceIcon moveUpLevelLeft"+WM.opts.littleButtonSize,onclick:function(){self.moveUpLevel();}})]),
					createElement("div",{className:"littleButton oddOrange", title:"Move Down Level"},[
						createElement("img",{className:"resourceIcon moveInLevel"+WM.opts.littleButtonSize,onclick:function(){self.moveDownLevel();}})]),
					createElement("div",{className:"littleButton oddBlue", title:"Show Source"},[
						createElement("img",{className:"resourceIcon object"+WM.opts.littleButtonSize,onclick:function(){promptText(JSON.stringify(self.saveableData),true);}})]),

					createElement("div",{className:"indent littleButton oddBlue", title:"Convert To Rule"},[
						createElement("img",{className:"resourceIcon exportGrab"+WM.opts.littleButtonSize,onclick:function(){self.convertToRule();}})]),

					createElement("div",{className:"indent littleButton "+((this.isGlobal)?"oddOrange":"oddGreen"), title:((this.isGlobal)?"Disable Profile Sharing":"Share With Other Profiles")},[
						this.toggleGlobalButton=createElement("img",{className:"resourceIcon "+((this.isGlobal)?"removeGlobal":"addGlobal")+WM.opts.littleButtonSize,onclick:function(){self.isGlobal=!self.isGlobal; WM.grabber.save();}})]),
				]),
				this.contentNode=createElement("div",{className:"subsection "+(this.expanded?"expanded":"collapsed")},[
					//appID
					createElement("div",{className:"line"},[
						createElement("label",{textContent:"appID:"}),
						this.appIDNode=createElement("input",{value:(this.appID||""), onchange:function(){self.appID=this.value;WM.grabber.save();self.populateBonusList();}}),
						this.appListNode=createElement("select",{onchange:function(){self.appIDNode.value=this.value; self.appID=this.value; WM.grabber.save(); self.populateBonusList();}}),
						createElement("div",{className:"littleButton oddBlue", title:"Refresh App List"},[
							createElement("img",{className:"resourceIcon refresh"+WM.opts.littleButtonSize,onclick:function(){self.populateAppList();}})]),
					]),
					//return type
					createElement("div",{className:"line"},[
						createElement("label",{textContent:"Return Type ('which'):"}),
						this.retNode=createElement("input",{value:(this.ret||"dynamic"), onchange:function(){self.ret=this.value;WM.grabber.save();}}),
						this.bonusNode=createElement("select",{onchange:function(){self.retNode.value=this.value; self.ret=this.value; WM.grabber.save();}}),
						createElement("div",{className:"littleButton oddBlue", title:"Refresh Bonus List"},[
							createElement("img",{className:"resourceIcon refresh"+WM.opts.littleButtonSize,onclick:function(){self.populateBonusList();}})]),
					]),
					//search list
					createElement("div",{className:"line"},[
						createElement("label",{textContent:"Search In Field(s):",title:"Specify fields in which to look for data. Adjust order as needed."}),
						this.searchNode=createElement("div",{className:"subsection optioncontainer"},(function(){
							var ret=[];
							
							//draw first the methods we have already selected
							if (isArrayAndNotEmpty(self.search)) for (var m=0; m<self.search.length; m++) {
								var s = self.search[m];
								ret.push(createElement("div",{className:"line"},[
									createElement("div",{className:"littleButton oddGreen", title:"Move Up"},[
										createElement("img",{className:"resourceIcon nomargin arrowUp16",onclick:function(){elementMoveUp(this.parentNode.parentNode); self.calcSearch();}})
									]),
									createElement("div",{className:"littleButton oddOrange", title:"Move Down"},[
										createElement("img",{className:"resourceIcon nomargin arrowDown16",onclick:function(){elementMoveDown(this.parentNode.parentNode); self.calcSearch();}})
									]),
									createElement("div",{className:"littleButton oddGreen", title:"Move To Top"},[
										createElement("img",{className:"resourceIcon nomargin moveTopLeft16",onclick:function(){elementMoveTop(this.parentNode.parentNode); self.calcSearch();}})
									]),
									createElement("div",{className:"littleButton oddOrange", title:"Move To Bottom"},[
										createElement("img",{className:"resourceIcon nomargin moveBottomLeft16",onclick:function(){elementMoveBottom(this.parentNode.parentNode); self.calcSearch();}})
									]),
									createElement("input",{type:"checkbox",value:s,checked:true,onchange:function(){self.calcSearch();}}),
									createElement("label",{textContent:s,title:WM.rulesManager.postParts[s]}),								
								]));
							}
							
							//draw the remaining items in their normal order
							for (var m=0; m<WM.grabber.methods.length; m++){
								var s = WM.grabber.methods[m];
								//prevent duplicates
								if (self.search.inArray(s)) continue;
								ret.push(createElement("div",{className:"line"},[
									createElement("div",{className:"littleButton oddGreen", title:"Move Up"},[
										createElement("img",{className:"resourceIcon nomargin arrowUp16",onclick:function(){elementMoveUp(this.parentNode.parentNode); self.calcSearch();}})
									]),
									createElement("div",{className:"littleButton oddOrange", title:"Move Down"},[
										createElement("img",{className:"resourceIcon nomargin arrowDown16",onclick:function(){elementMoveDown(this.parentNode.parentNode); self.calcSearch();}})
									]),
									createElement("div",{className:"littleButton oddGreen", title:"Move To Top"},[
										createElement("img",{className:"resourceIcon nomargin moveTopLeft16",onclick:function(){elementMoveTop(this.parentNode.parentNode); self.calcSearch();}})
									]),
									createElement("div",{className:"littleButton oddOrange", title:"Move To Bottom"},[
										createElement("img",{className:"resourceIcon nomargin moveBottomLeft16",onclick:function(){elementMoveBottom(this.parentNode.parentNode); self.calcSearch();}})
									]),
									createElement("input",{type:"checkbox",value:s,onchange:function(){self.calcSearch();}}),
									createElement("label",{textContent:s,title:WM.rulesManager.postParts[s]}),
								]));
							}
							
							return ret;
						})()),
					]),
					//find mode
					createElement("div",{className:"line"},[
						createElement("label",{textContent:"Find Mode:",title:"Choose the mode you will use to find text."}),
						this.findModeNode=createElement("select",{onchange:function(){self.findMode=this.value;}},[
							createElement("option",{selected:(this.findMode=="basic"),value:"basic",textContent:"Basic",title:"Search for a list of words or phrases."}),
							createElement("option",{selected:(this.findMode=="subnumrange"),value:"subnumrange",textContent:"Number Range",title:"Search for a range of numbers using an insertion point '{%1}' in your find parameter."}),
							createElement("option",{selected:(this.findMode=="subtests"),value:"subtests",textContent:"Sub Tests",title:"Search for a list of words or phrases using an insertion point '{%1}' in your find parameter."}),
							createElement("option",{selected:(this.findMode=="regex"),value:"regex",textContent:"Registered Expression",title:"Search for complex phrases using a regular expression."})
						]),
					]),
					//find list
					createElement("div",{className:"line"},[
						createElement("label",{textContent:"Find:",title:"One per line (basic mode), or a single regular expression. First match is used, so mind the order."}),
						createElement("div",{className:"subsection"},[
							this.findNode=createElement("textarea",{className:"fit",textContent:((this.findMode=="basic")?this.findArray.join("\n"):this.find), onchange:function(){
								if (self.findMode=="basic") self.findArray=this.value.split("\n");
								else self.find=this.value;
								WM.grabber.save();
							}}),
						])
					]),
					//subtests list
					this.subTestsBoxNode=createElement("div",{className:("line").toggleWordB(this.findMode!="subtests","hidden")},[
						createElement("label",{textContent:"Subtest Texts:",title:"Provide text replacements for the insertion point. No regular expressions."}),
						createElement("div",{className:"subsection"},[
							this.subTestsNode=createElement("textarea",{className:"fit",textContent:((isArray(this.subTests)?this.subTests.join("\n"):"")||""), onchange:function(){self.subTests=this.value.split("\n"); WM.grabber.save();}}),
						])
					]),
					//subnumrange picker
					this.subNumRangeBoxNode=createElement("div",{className:("line").toggleWordB(this.findMode!="subnumrange","hidden")},[
						createElement("label",{textContent:"Subtest Number Range:",title:"Provide a start and end range for the insertion point."}),
						this.subNumRangeLowNode=createElement("input",{value:this.subNumRange.low||0, onchange:function(){self.subNumRange.low=this.value; WM.grabber.save();}}),
						this.subNumRangeHighNode=createElement("input",{value:this.subNumRange.high||0, onchange:function(){self.subNumRange.high=this.value; WM.grabber.save();}}),
					]),
					//kids subbox
					createElement("div",{className:"line"},[
						createElement("label",{textContent:"Child Tests:",title:"Child tests are nested tests which are applied to matching posts at the same time the parent test is applied. Child rules can have different return values that override the parent return value."}),
						createElement("div",{className:"littleButton oddGreen",onclick:function(){self.addChild();},title:"Add Child"},[
							createElement("img",{className:"resourceIcon plus"+WM.opts.littleButtonSize}),
						]),
						this.kidsNode=createElement("div",{className:"subsection"}),
					]),
				]),
			])
		);}catch(e){log("WM.Test.init.drawTest: "+e);}
		
		//populate my bonus list
		this.populateAppList();
		this.populateBonusList();

		//list the kids for this test
		if (isArrayAndNotEmpty(params.kids)) for (var i=0,kid; (kid=params.kids[i]); i++) {
			this.addChild(kid);
		}
		
		return self;
	}catch(e){log("WM.Test.init: ")+e}};

//***************************************************************************************************************************************
//***** Feed Objects
//***************************************************************************************************************************************
	WM.feedManager = {
		feeds : [],
		
		init : function(){
			var feedsIn=(getOptJSON("feeds3_"+WM.currentUser.profile)||[]);
			var feedsInGlobal=(getOptJSON("feeds3_global")||[]);
			
			if (isArrayAndNotEmpty(feedsIn)) {
				//import feeds from storage
				for (var f=0;f<feedsIn.length;f++){
					feed=feedsIn[f];
					if (!feed.isGlobal){
						WM.feedManager.feeds.push(new WM.Feed(feed));
					} else {
						var glob=feedsInGlobal[feed.uniqueID]||null;
						if (glob){
							var merge=mergeJSON(glob,feed);
							WM.feedManager.newFeed(merge);
							glob.alreadyUsed=true;
						} else {
							log("WM.feedManager.init: Global feed missing, cannot merge");
						}
					}
				}
			} else {
				//never been used before, create base feeds
				WM.feedManager.feeds.push(new WM.Feed({title:"My Home Feed", url:"https://graph.facebook.com/me/home", isRemoveable:false}));
				//WM.feedManager.feeds.push(new WM.Feed({title:"My Profile Wall", url:"https://graph.facebook.com/me/feed", isRemoveable:false}));
				
				//import oldstyle feeds
				var feedsOld=getOpt("feeds_"+WM.currentUser.profile);
				if (feedsOld){
					feedsOld=feedsOld.split("\n");
					if (isArrayAndNotEmpty(feedsOld)) for (var f=0;f<feedsOld.length;f++) {
						//prevent empties
						if (feedsOld[f]) {
							//create the new feed
							WM.feedManager.newFeed({id:feedsOld[f],title:feedsOld[f]});
						}
					}
				}
				WM.feedManager.save();
			}
			//import all global feeds not already accounted for
			for (var t in feedsInGlobal) {
				var glob=feedsInGlobal[t];
				if (!glob.alreadyUsed){
					glob.uniqueID=t;
					glob.isGlobal=true;
					WM.feedManager.newFeed(glob); //newFeed adds app filters, where New Feed() does not
				}
			}
		},
		
		newFeed : function(params){
			params=params||{};
			var feed = new WM.Feed(params);
			WM.feedManager.feeds.push(feed);
			
			//add filters for each app available
			for (var a in WM.apps){
				feed.addFilter({id:"app_"+a});
			}
		},
		
		save :function(){
			var retFeeds=[];
			var retGlobal={};
			
			if (isArrayAndNotEmpty(WM.feedManager.feeds)) for (var f=0,len=WM.feedManager.feeds.length; f<len; f++){
				var feed=WM.feedManager.feeds[f];
				if (!feed.isGlobal) {
					retFeeds.push(feed.saveableData);
				} else {
					retFeeds.push({isGlobal:true, uniqueID:feed.uniqueID, enabled:feed.enabled, expanded:feed.expanded});
					var glob=feed.saveableData;
					glob.uniqueID=feed.uniqueID;
					retGlobal[feed.uniqueID]=glob;
				}
			}
			setOptJSON("feeds3_"+WM.currentUser.profile,retFeeds);
			setOptJSON("feeds3_global",retGlobal);
		},
		
	};

//***************************************************************************************************************************************
//***** FeedFilter Class
//***************************************************************************************************************************************
	WM.FeedFilter = function(params){try{
		this.objType="feedFilter";
		params=params||{};
		var self=this;
		
		//set defaults
		this.enabled=true;
		this.expanded=true;
		this._olderLimitReached=false;
		//initialize edges to the collector startup time
		this.oldedge=Math.round(timeStamp()/1000); //older edge timestamp
		this.newedge=Math.round(timeStamp()/1000); //newer edge timestamp
		
		//use passed params
		for (var p in params) this[p]=params[p];

		this.enable=function(){try{
			this.enabled=true;
			this.node.className=this.node.className.removeWord("disabled");
			WM.feedManager.save();
		}catch(e){log("WM.FeedFilter.enable: "+e);}};

		this.disable=function(){try{
			this.enabled=false;
			this.node.className=this.node.className.addWord("disabled");
			WM.feedManager.save();
		}catch(e){log("WM.FeedFilter.disable: "+e);}};

		this.toggle=function(){try{
			this.enabled=this.toggleNode.checked;
			this.node.className=this.node.className.swapWordB(this.enabled,"enabled","disabled");
			WM.feedManager.save();
		}catch(e){log("WM.FeedFilter.toggle: "+e);}};

		this.toggleContent=function(){try{
			this.expanded=!this.expanded;
			var btnSize=WM.opts.littleButtonSize;
			with (this.contentNode)
				className=className.swapWordB(this.expanded,"expanded","collapsed");
			with (this.toggleImgNode)
				className=className.swapWordB(this.expanded,"treeCollapse"+btnSize,"treeExpand"+btnSize);
			WM.feedManager.save();
		}catch(e){log("WM.FeedFilter.toggleContent: "+e);}};
		
		//remove this
		this.remove=function(){try{
			if (this.node) remove(this.node);
			if (this.parent) delete this.parent.filters[this.id];
			WM.feedManager.save();
		}catch(e){log("WM.FeedFilter.remove: "+e);}};
		
		//fetch posts for this
		this.fetchNewer=function(){try{
			WM.fetch({
				newer:true,
				apps:WM.apps[this.appID],
				feeds:[this.parent],
				bypassPause:true,
				bypassAppDisabled:true,
				bypassFeedDisabled:true,
				bypassFilterDisabled:true,
			});
		}catch(e){log("WM.FeedFilter.fetchNewer: "+e);}};

		this.fetchOlder=function(){try{
			WM.fetch({
				older:true,
				apps:WM.apps[this.appID],
				feeds:[this.parent],
				bypassPause:true,
				bypassAppDisabled:true,
				bypassFeedDisabled:true,
				bypassFilterDisabled:true,
			});
		}catch(e){log("WM.FeedFilter.fetchOlder: "+e);}};
		
		this.__defineGetter__("olderLimitReached",function(){try{
			return this._olderLimitReached;
		}catch(e){log("WM.FeedFilter.olderLimitReached: "+e);}});
		this.__defineSetter__("olderLimitReached",function(v){try{
			this._olderLimitReached=v;
			//update the sidekick page button graphics
			var node=this.olderLimitNode;
			if (node) node.textContent=v;
			if (v) {
				WM.rulesManager.doEvent("onFeedFilterOlderLimitReached",this);
			}
		}catch(e){log("WM.FeedFilter.olderLimitReached: "+e);}});

		this.__defineGetter__("appID",function(){try{
			//this assumes its an app filter because so far thats all we use
			return this.id.removePrefix("app_");
		}catch(e){log("WM.FeedFilter.appID: "+e);}});

		this.__defineGetter__("appName",function(){try{
			//this assumes its an app filter because so far thats all we use
			//it also assumes app objects are global, which they are
			//but that they are also available and already filled in by the sidekick, which they may not be
			var a = WM.apps[this.appID];
			if (a!=undefined) {
				//debug.print([this.appID,a]);
				return a.name;
			}
			return "";
		}catch(e){log("WM.FeedFilter.appName: "+e);}});
		
		//draw it
		try{
			if (this.parent.filtersNode) this.parent.filtersNode.appendChild(
				this.node=createElement("div",{className:"listItem "+((this.enabled)?"enabled":"disabled")},[
					createElement("div",{className:"line"},[
						createElement("div",{className:"littleButton",title:"Toggle Content",onclick:function(){self.toggleContent();}},[
							this.toggleImgNode=createElement("img",{className:"resourceIcon "+(this.expanded?"treeCollapse"+WM.opts.littleButtonSize:"treeExpand"+WM.opts.littleButtonSize)}),
						]),
						this.toggleNode=createElement("input",{type:"checkbox",checked:this.enabled,onchange:function(){
							self.enabled=this.checked;
							with (self.node) className=className.toggleWordB(!this.checked,"disabled");
							WM.feedManager.save();
						}}),
						createElement("span",{textContent:this.appName + " (" + this.id + ")"}),
						
						//toolbox
						createElement("div",{className:"littleButton oddBlue", title:"Fetch Newer"},[
							this.fetchNewerButton=createElement("img",{className:"resourceIcon rssUpRight"+WM.opts.littleButtonSize,onclick:function(){self.fetchNewer();} })
						]),
						createElement("div",{className:"littleButton", title:"Fetch Older"},[
							this.fetchOlderButton=createElement("img",{className:"resourceIcon rssDownLeft"+WM.opts.littleButtonSize,onclick:function(){self.fetchOlder();} })
						]),
					]),
					this.contentNode=createElement("div",{className:"subsection "+(this.expanded?"expanded":"collapsed")},[
						createElement("div",{className:"line"},[
							createElement("label",{textContent:"Older Limit Reached: ",title:"Reports if this filter has reached the user defined oldest post limit."}),
							this.olderLimitNode=createElement("span",{textContent:this.olderLimitReached}),
						]),
						createElement("div",{className:"line"},[
							createElement("label",{textContent:"Newer Edge: ",title:"A Unixtime indicator of the newest post-time you have fetched for this filter."}),
							this.newedgeNode=createElement("span",{textContent:this.newedge}),
						]),
						createElement("div",{className:"line"},[
							createElement("label",{textContent:"Older Edge: ",title:"A Unixtime indicator of the oldest post-time you have fetched for this filter."}),
							this.oldedgeNode=createElement("span",{textContent:this.oldedge}),
						]),
					]),
				])
			);
		}catch(e){log("WM.FeedFilter.init:addManagerElement: "+e);};				
		
		return self;
	}catch(e){log("WM.FeedFilter.init: "+e);}};

//***************************************************************************************************************************************
//***** Feed Class
//***************************************************************************************************************************************
	WM.Feed = function(params){try{
		this.objType="feed";
		params=params||{};
		var self=this;

		//set defaults
		this.enabled=true;
		this.expanded=true;
		this.url="";
		this.id="";
		this.filters={};
		this.feedName="";
		this.isRemoveable=true; //set to false on own feeds
		this.title="New Feed";
		this._isGlobal=false;

		//use passed params
		var newFilters=params.filters||{};
		delete params.filters;
		
		this.__defineGetter__("isGlobal",function(){try{
			return this._isGlobal;
		}catch(e){log("WM.Feed.isGlobal: "+e);}});
		this.__defineSetter__("isGlobal",function(v){try{			
			if (!v) {
				if (!confirm("Disabling profile sharing on this feed will prevent other users on this machine from loading it. Are you sure you wish to make this feed locally available only?")) return;
			}
		
			this._isGlobal=v;
			
			//make sure we have a uniqueID
			//but don't destroy one that already exists
			if (v && !exists(this.uniqueID)) this.uniqueID = unique();
			
			//change the color/icon of the isGlobal button
			if (this.toggleGlobalButton) {
				var s=WM.opts.littleButtonSize;
				with (this.toggleGlobalButton) className=className.swapWordB(v,"removeGlobal"+s,"addGlobal"+s);
				with (this.toggleGlobalButton.parentNode) {
					className=className.swapWordB(v,"oddOrange","oddGreen");
					title=(v)?"Disable Profile Sharing":"Share With Other Profiles";
				}
			}
		}catch(e){log("WM.Feed.isGlobal: "+e);}});
		
		this.__defineGetter__("saveableData",function(){try{
			var ret={};
			
			ret.title=this.title;
			ret.enabled=this.enabled;
			ret.expanded=this.expanded;
			ret.isRemoveable=this.isRemoveable;
			ret.url=this.url;
			if (this.isRemoveable) ret.id=this.id;
			
			//capture filters
			ret.filters={};
			for (var f in this.filters) {
				ret.filters[f]={
					enabled:this.filters[f].enabled,
					expanded:this.filters[f].expanded,
					id:this.filters[f].id,
				};
			}
			
			return ret;
		}catch(e){log("WM.Feed.saveableData: "+e);}});		
				
		for (var p in params) this[p]=params[p];

		this.enable=function(){try{
			this.enabled=true;
			this.node.className=this.node.className.removeWord("disabled");
			WM.feedManager.save();
		}catch(e){log("WM.Feed.enable: "+e);}};

		this.disable=function(){try{
			this.enabled=false;
			this.node.className=this.node.className.addWord("disabled");
			WM.feedManager.save();
		}catch(e){log("WM.Feed.disable: "+e);}};

		this.toggle=function(){try{
			this.enabled=this.toggleNode.checked;
			this.node.className=this.node.className.swapWordB(this.enabled,"enabled","disabled");
			WM.feedManager.save();
		}catch(e){log("WM.Feed.toggle: "+e);}};

		//create a filter for a specific app
		//filter id must be "app_"+appID
		//will not add duplicates
		this.addFilter=function(params){try{
			var isNew=!exists(params);
			params=params||{};
			params.parent=this;
			
			//prevent duplicates
			if (!exists(this.filters[params.id])) {
				return (this.filters[params.id]=new WM.FeedFilter(params));
				if (isNew) WM.feedManager.save();
			} else {
				return this.filters[params.id];
			}
		}catch(e){log("WM.Feed.addFilter: "+e);}};	

		//get the extents of the feed by merging all feed filter oldedge/newedge values
		this.getMergedEdges=function(params){
			/*
				apps[]: an array of appID's to test against, otherwise read from all filters
			*/
			
			//console.log("getMergedEdges: "+JSON.stringify(params));
			
			
			var retval = {newedge:Math.round(timeStamp()/1000), oldedge:0};
			
			if (params.apps||null){
				for (var c=0,l=params.apps.length;c<l;c++){
					var filter = this.filters["app_"+params.apps[c]];
					if (filter||null){
						//get the youngest older edge and oldest newer edge so we don't lose posts because one feed is more active.
						//this forces them to run at the same edges after the first pull
						retval.newedge = Math.min(retval.newedge, filter.newedge);
						retval.oldedge = Math.max(retval.oldedge, filter.oldedge);
					} else {
						log("getMergedEdges: no filter matching app_"+params.apps[c]+" on feed " + this.id);
					}
				}
			} else {
				for (var name in this.filters){
					var filter = this.filters[name];
					if (filter||null){
						//get the youngest older edge and oldest newer edge so we don't lose posts because one feed is more active.
						//this forces them to run at the same edges after the first pull
						retval.newedge = Math.min(retval.newedge, filter.newedge);
						retval.oldedge = Math.max(retval.oldedge, filter.oldedge);
					} else {
						log("getMergedEdges: no filter matching "+name+" on feed " + this.id);
					}
				}
				
			}
			
			return retval;
			
		};
		
		//remove this
		this.remove=function(noConfirm){try{
			if (this.isRemoveable) {
				var ask=WM.opts.feedsConfirmDeleteFeed;
				if (noConfirm || (this.isGlobal && confirm("This feed is shared with other profiles. Deleting it here will prevent it from loading for other users. Are you sure you wish to delete this feed and its filters.")) || !ask || (!this.isGlobal && ask && confirm("Delete feed and all of its filters?"))){
					//remove my data
					if (this.node) remove(this.node);
					WM.feedManager.feeds.removeByValue(this);
					
					WM.feedManager.save();
				}
			}
		}catch(e){log("WM.Feed.remove: "+e);}};

		//fetch posts for this
		this.fetchNewer=function(){try{
			WM.fetch({
				newer:true,
				feeds:[self],
				bypassPause:true,
				bypassAppDisabled:true,
				bypassFeedDisabled:true,
			});
		}catch(e){log("WM.Feed.fetchNewer: "+e);}};

		this.fetchOlder=function(){try{
			WM.fetch({
				older:true,
				feeds:[self],
				bypassPause:true,
				bypassAppDisabled:true,
				bypassFeedDisabled:true,
			});
		}catch(e){log("WM.Feed.fetchOlder: "+e);}};

		this.toggleContent=function(){try{
			this.expanded=!this.expanded;
			var btnSize=WM.opts.littleButtonSize;
			with (this.contentNode)
				className=className.swapWordB(this.expanded,"expanded","collapsed");
			with (this.toggleImgNode)
				className=className.swapWordB(this.expanded,"treeCollapse"+btnSize,"treeExpand"+btnSize);
			WM.feedManager.save();
		}catch(e){log("WM.Feed.toggleContent: "+e);}};

		if (this.id && !this.url) this.url="https://graph.facebook.com/"+this.id+"/feed";
		
		//draw it
		try{
			WM.console.feedManagerNode.appendChild(
				this.node=createElement("div",{className:"listItem "+((this.enabled)?"enabled":"disabled")},[
					createElement("div",{className:"line"},[
						createElement("div",{className:"littleButton",title:"Toggle Content",onclick:function(){self.toggleContent();}},[
							this.toggleImgNode=createElement("img",{className:"resourceIcon "+(this.expanded?"treeCollapse"+WM.opts.littleButtonSize:"treeExpand"+WM.opts.littleButtonSize)}),
						]),
						this.toggleNode=createElement("input",{type:"checkbox",checked:this.enabled,onchange:function(){
							self.enabled=this.checked; 
							with (self.node) className=className.toggleWordB(!this.checked,"disabled");
							WM.feedManager.save();
						}}),
						this.titleNode=createElement("input",{value:(this.title||""), onchange:function(){self.title=this.value; WM.feedManager.save();}}),
						
						//toolbox
						createElement("div",{className:"littleButton oddBlue", title:"Fetch Newer"},[
							this.fetchNewerButton=createElement("img",{className:"resourceIcon rssUpRight"+WM.opts.littleButtonSize,onclick:function(){self.fetchNewer();} })
						]),
						createElement("div",{className:"littleButton", title:"Fetch Older"},[
							this.fetchOlderButton=createElement("img",{className:"resourceIcon rssDownLeft"+WM.opts.littleButtonSize,onclick:function(){self.fetchOlder();} })
						]),
						(this.isRemoveable)?createElement("div",{className:"littleButton oddOrange", title:"Remove Feed"},[
							this.removeButtonNode=createElement("img",{className:"resourceIcon trash"+WM.opts.littleButtonSize,onclick:function(){self.remove();} })
						]):null,
						
						(this.isRemoveable)?createElement("div",{className:"indent littleButton "+((this.isGlobal)?"oddOrange":"oddGreen"), title:((this.isGlobal)?"Disable Profile Sharing":"Share With Other Profiles")},[
							this.toggleGlobalButton=createElement("img",{className:"resourceIcon "+((this.isGlobal)?"removeGlobal":"addGlobal")+WM.opts.littleButtonSize,onclick:function(){self.isGlobal=!self.isGlobal; WM.feedManager.save();}})
						]):null,

					]),
					this.contentNode=createElement("div",{className:"subsection "+(this.expanded?"expanded":"collapsed")},[
						(this.isRemoveable)?createElement("div",{className:"line"},[
							createElement("label",{textContent:"Target FB Entity: ",title:"The request address from where WM gets posts for this fb entity."}),
							this.idNode=createElement("input",{value:(this.id||""), onchange:function(){
								self.id=this.value;
								self.url="https://graph.facebook.com/"+this.value+"/feed";
								self.urlNode.textContent=self.url;
								WM.feedManager.save();
							}}),
							createElement("label",{textContent:"URL: ",title:"The request address from where WM gets posts for this fb entity."}),
							this.urlNode=createElement("span",{textContent:this.url}),
						]):null,
						//app filters sub box
						createElement("div",{className:"line"},[
							createElement("label",{textContent:"App Filters: ",title:"This is a list of filters run on this feed by apps you collect for. Only filters for sidekick-supported apps are used."}),
							this.filtersNode=createElement("div",{className:"subsection"}),
						]),
					]),
				])
			);
		}catch(e){log("WM.Feed.init:addManagerElement: "+e);};		
		
		//add any passed filters
		for (var f in newFilters){
			this.addFilter(newFilters[f]);
		}
		
		return self;
	}catch(e){log("WM.Feed.init: "+e);}};
	
//***************************************************************************************************************************************
//***** Friend Objects
//***************************************************************************************************************************************
	WM.friendTracker = {
		friends: {},
		
		init : function(){
			//import friends tracker data
			var friendsIn=getOptJSON('friends_'+WM.currentUser.profile)||[];
			if (isArrayAndNotEmpty(friendsIn)) for (var f=0,len=friendsIn.length;f<len;f++) {
				WM.friendTracker.newFriend(friendsIn[f],true);
			}
			WM.friendTracker.sort();
		},
		
		clean : function(){
			//clean friend tracker data
			var len=0;
			if (WM.opts.useFriendTracker && (len=WM.friendTracker.friends.length)) {
				var ageDays=WM.opts.trackDays*day;
				var timeNow=timeStamp();
				for (var f=0; f<len; f++){
					var friend=WM.friendTracker.friends[f];
					if (friend.data && friend.data.posts){
						for (var p in friend.data.posts){
							var post=friend.data.posts[p];
							if ((timeNow-(post.date*1000)) > ageDays) {
								delete friend.data.posts[p];
							}
						}
					}
				}
			}
		},
		
		clearAll : function(noConfirm){
			var ask=WM.opts.trackConfirmClearUser;
			if (noConfirm || !ask || (ask && confirm("Clear tracker history for all users?"))){
				for (var f in WM.friendTracker.friends){
					WM.friendTracker.friends[f].remove(true);
				}
			}			
		},
		
		newFriend : function(params,preventSort){
			params=params||{};
			var friend = new WM.Friend(params);
			WM.friendTracker.friends[friend.id]=friend;
			if (!preventSort) WM.friendTracker.sort();
			return friend;
		},

		save :function(){
			var ret=[];
			for (var f in WM.friendTracker.friends){
				ret.push(WM.friendTracker.friends[f].saveableData);
			}
			setOptJSON("friends_"+WM.currentUser.profile,ret);
		},
		
		sort : function(params){
			params=params||{};
			
			if (exists(params.sortBy)) WM.quickOpts.sortFriendsBy=params.sortBy;
			if (exists(params.sortOrder)) WM.quickOpts.sortFriendsOrder=params.sortOrder;
			WM.saveQuickOpts();

			var sortBy=params.sortBy||WM.quickOpts.sortFriendsBy||"name"
			var sortOrder=params.sortOrd||WM.quickOpts.sortFriendsOrder||"asc"
			
			var friendArray=[];
			for (var f in WM.friendTracker.friends) {
				friend=WM.friendTracker.friends[f];
				friendArray.push({id:friend[sortBy],node:friend.node});
			}
			
			if (["asc","ascending"].inArray(sortOrder)) friendArray.sort(function(a,b){return a.id>b.id;});
			else if (["desc","descending"].inArray(sortOrder)) friendArray.sort(function(a,b){return a.id<b.id;});
			
			for (var f=0,len=friendArray.length; f<len; f++) {
				WM.console.friendBuild.appendChild(friendArray[f].node);
			}
		},
		
		track : function(post){
			//dont track stuff older than our older tracking limit
			var limit=WM.opts.trackTime*day;
			if ( ( timeStamp()-(post.date*1000) ) < limit ) {
				//get/create the friend record
				var friend=WM.friendTracker.friends[post.fromID]||null;
				if (!friend) {
					friend=WM.friendTracker.newFriend({id:post.fromID,name:post.fromNameLastFirst});
				}
				//check if this is newer than last known post
				if (WM.opts.trackLastKnownPost) {
					var data=friend.lastKnownPost;
					if (data) {
						if (data.date<post.date){
							data.date=post.date;
							//data.id=post.id.removePrefix(post.fromID+"_");
						}
					} else {
						friend.data.lastKnownPost={date:post.date};
					}
				}
				//add it to history
				if (WM.opts.trackCreated){
					var data={date:post.date};
					if (WM.opts.trackFailed){
						data.failed=(post.status<0 && post.status !=-4 && post.status !=-6);
					}
					if (WM.opts.trackAccepted){
						data.accepted=(post.status>0 || post.status ==-4 || post.status ==-6);
					}
					friend.data.posts[post.id.removePrefix(post.fromID+"_")]=data;
				}
				//save it
				friend.updateStats();
				WM.friendTracker.save();
				
				//push events
				WM.rulesManager.doEvent("onFriendDataChanged",friend);
			}
		},
		
		trackStatus : function(post,acceptOrFail){
			var friend=WM.friendTracker.friends[post.fromID]||null;
			if (friend) {
				var data=friend.data.posts[post.id.removePrefix(post.fromID+"_")]||null;
				if (data){
					if (acceptOrFail) {
						data.accepted=true;
						delete data.failed;
					} else {
						data.failed=true;
						delete data.accepted;
					}
					friend.updateStats();
					WM.rulesManager.doEvent("onFriendDataChanged",friend);
				} else {
					debug.print("post does not exist under friend");
					//if post does not exists, we had more errors elsewhere
					//or post id not fit our history range
				}
			} else {
				debug.print("friend does not exist for this post");
				//if friend does not exist, we had errors elsewhere
				//don't bother fixing it here
			}
		},
	};

//***************************************************************************************************************************************
//***** Friend Class
//***************************************************************************************************************************************
	WM.Friend = function(params){try{
		this.objType="friend";
		params=params||{};
		var self=this;

		//set defaults
		this.expanded=false;
		this.id="";
		this.name="";
		
		this.data={
			lastKnownPost:{date:0},
			posts:{},
		};

		this.__defineGetter__("saveableData",function(){try{
			var ret={};
			
			ret.id=this.id;
			ret.name=this.name;
			ret.enabled=this.enabled;
			ret.expanded=this.expanded;
			
			//capture posts data
			ret.data=this.data;
			
			return ret;
		}catch(e){log("WM.Friend.saveableData: "+e);}});		
						
		for (var p in params) this[p]=params[p];

		//remove this
		this.remove=function(noConfirm){try{
			var ask=WM.opts.trackConfirmClearUser;
			if (noConfirm || !ask || (ask && confirm("Clear history for this user?"))){
				//remove my data
				if (this.node) remove(this.node);
				delete WM.friendTracker.friends[this.id];					
				WM.friendTracker.save();
			}
		}catch(e){log("WM.Friend.remove: "+e);}};

		this.toggleContent=function(){try{
			this.expanded=!this.expanded;
			var btnSize=WM.opts.littleButtonSize;
			with (this.contentNode)
				className=className.swapWordB(this.expanded,"expanded","collapsed");
			with (this.toggleImgNode)
				className=className.swapWordB(this.expanded,"treeCollapse"+btnSize,"treeExpand"+btnSize);
			WM.friendTracker.save();
		}catch(e){log("WM.Friend.toggleContent: "+e);}};
		
		this.addToFeeds=function(){try{
			WM.feedManager.newFeed({id:this.id, title:this.name});
			WM.feedManager.save();
		}catch(e){log("WM.Friend.addToFeeds: "+e);}};
		
		this.countAccepted=function(){try{
			var c=0;
			if (this.data.posts) for (var p in this.data.posts) {
				var post=this.data.posts[p];
				if (post.accepted) c++;
			}
			return c;
		}catch(e){log("WM.Friend.countAccepted: "+e);}};

		this.countFailed=function(){try{
			var c=0;
			if (this.data.posts) for (var p in this.data.posts) {
				var post=this.data.posts[p];
				if (post.failed) c++;
			}
			return c;
		}catch(e){log("WM.Friend.countFailed: "+e);}};

		this.countCreated=function(){try{
			var c=0;
			if (this.data.posts) for (var p in this.data.posts) {
				c++
			}
			return c;
		}catch(e){log("WM.Friend.countFailed: "+e);}};

		this.__defineGetter__("lastKnownPost",function(){try{
			if (this.data && (this.data.lastKnownPost||null)){
				return this.data.lastKnownPost;
			}
			return {id:null,date:0};
		}catch(e){log("WM.Friend.lastKnownPost: "+e);}});
		this.__defineGetter__("lastKnownPostDate",function(){try{
			if (this.data && (this.data.lastKnownPost||null)){
				return this.data.lastKnownPost.date;
			}
			return 0;
		}catch(e){log("WM.Friend.lastKnownPostDate: "+e);}});
		this.__defineGetter__("acceptCount",function(){try{
			return this.countAccepted();
		}catch(e){log("WM.Friend.acceptCount: "+e);}});
		this.__defineGetter__("failCount",function(){try{
			return this.countFailed();
		}catch(e){log("WM.Friend.failCount: "+e);}});
		this.__defineGetter__("postCount",function(){try{
			return this.countCreated();
		}catch(e){log("WM.Friend.postCount: "+e);}});
		this.__defineGetter__("totalCount",function(){try{
			return this.failCount+this.acceptCount;
		}catch(e){log("WM.Friend.totalCount: "+e);}});
		
		this.updateStats=function(){try{
			var n=this.statsNode;
			if (n) {
				if (WM.opts.trackLastKnownPost){
					d=new Date(((this.lastKnownPost.date*1000)||0)).toLocaleString();
					if (!this.lastPostNode) {
						n.appendChild(createElement("div",{className:"line"},[
							createElement("label",{textContent:"Last Known Post Date: "}),
							this.lastPostNode=createElement("span",{textContent:d})
						]));
					} else {
						this.lastPostNode.textContent=d;
					}
				}

				if (WM.opts.trackCreated){
					if (!this.countCreatedNode) {
						n.appendChild(createElement("div",{className:"line"},[
							createElement("label",{textContent:"Posts Created: "}),
							this.countCreatedNode=createElement("span",{textContent:this.countCreated()})
						]));
					} else {
						this.countCreatedNode.textContent=this.countCreated();
					}
				}
				
				if (WM.opts.trackAccepted){
					if (!this.countAcceptedNode){
						n.appendChild(createElement("div",{className:"line"},[
							createElement("label",{textContent:"Posts Accepted: "}),
							this.countAcceptedNode=createElement("span",{textContent:this.countAccepted()})
						]));
					} else {
						this.countAcceptedNode.textContent=this.countAccepted();
					}
				}
				
				if (WM.opts.trackFailed){
					if (!this.countFailedNode){
						n.appendChild(createElement("div",{className:"line"},[
							createElement("label",{textContent:"Posts Failed: "}),
							this.countFailedNode=createElement("span",{textContent:this.countFailed()})
						]));
					} else {
						this.countFailedNode.textContent=this.countFailed();
					}
				}
			}
		}catch(e){log("WM.Friend.updateStats: "+e);}};
		
		//draw it
		try{
			WM.console.friendBuild.appendChild(
				this.node=createElement("div",{className:"listItem"},[
					createElement("div",{className:"line"},[
						createElement("div",{className:"littleButton",title:"Toggle Content",onclick:function(){self.toggleContent();}},[
							this.toggleImgNode=createElement("img",{className:"resourceIcon "+(this.expanded?"treeCollapse"+WM.opts.littleButtonSize:"treeExpand"+WM.opts.littleButtonSize)}),
						]),
						this.titleNode=createElement("input",{value:(this.name||""), onchange:function(){self.name=this.value; WM.friendTracker.save();}}),
						
						//toolbox
						createElement("div",{className:"littleButton", title:"Add To Feeds"},[
							createElement("img",{className:"resourceIcon addFeed"+WM.opts.littleButtonSize,onclick:function(){self.addToFeeds();} })
						]),
						createElement("div",{className:"littleButton oddOrange", title:"Clear Data"},[
							createElement("img",{className:"resourceIcon trash"+WM.opts.littleButtonSize,onclick:function(){self.remove();} })
						]),
						createElement("div",{onclick:function(){window.open("http://www.facebook.com/profile.php?id="+self.id,"_blank");},title:"Visit Wall",className:"littleButton oddBlue"},[
							createElement("img",{className:"resourceIcon openInNewWindow"+WM.opts.littleButtonSize})
						]),
					]),
					this.contentNode=createElement("div",{className:"subsection "+(this.expanded?"expanded":"collapsed")},[
						createElement("div",{className:"line"},[
							createElement("label",{textContent:"ID: ",title:"The facebook id of this user."}),
							createElement("span",{textContent:self.id}),
						]),
						//post data sub box
						createElement("div",{className:"line"},[
							createElement("label",{textContent:"Statistics: ",title:"Statistics you selected to track."}),
							this.statsNode=createElement("div",{className:"subsection"}),
						]),
					]),
				])
			);
		}catch(e){log("WM.Friend.init:addManagerElement: "+e);};	

		this.updateStats();
		
		return self;
	}catch(e){log("WM.Friend.init: "+e);}};
	
//***************************************************************************************************************************************
//***** Rules Manager Object
//***************************************************************************************************************************************
	WM.rulesManager = {
			
		rules:[],
		
		enabled:function(){return !WM.quickOpts.heartbeatDisabled;},

		init:function(params){try{
			params=(params||{});
			
			// build a kidsNode getter
			WM.rulesManager.__defineGetter__("kidsNode",function(){try{
				return $("wmPriorityBuilder");
			}catch(e){log("WM.rulesManager.kidsNode: "+e);}});

			//import rules
			WM.rulesManager.rules=[];
			var rulesIn=getOptJSON("priority3_"+WM.currentUser.profile)||{};
			var globalsIn=getOptJSON("priority3_global")||{};
			
			//detect early beta rule lists
			if (isObject(rulesIn)) for (var i in rulesIn){
				var rule=rulesIn[i];
				WM.rulesManager.rules.push( new WM.rulesManager.Rule(rule) );
				//don't bother with globals here
				
			//or use current version rule arrays
			} else if (isArrayAndNotEmpty(rulesIn)) for (var i=0,rule;(rule=rulesIn[i]);i++) {
				if (rule.isGlobal) {
					var glob=globalsIn[rule.uniqueID]||null;
					if (glob){
						var merge=mergeJSON(glob,rule);
						WM.rulesManager.rules.push( new WM.rulesManager.Rule(merge) );
						glob.alreadyUsed=true;
					} else {
						log("WM.rulesManager.init: Global rule missing, cannot merge");
					}
				} else {
					WM.rulesManager.rules.push( new WM.rulesManager.Rule(rule) );
				}
				
			}
			
			//import all globals not already accounted for
			for (var t in globalsIn) {
				var glob=globalsIn[t];
				//avoid already imported globals
				if (!glob.alreadyUsed){
					glob.uniqueID=t;
					glob.isGlobal=true;
					WM.rulesManager.rule.push( new WM.rulesManager.Rule(glob) );
				}
			}
			
			
		}catch(e){log("WM.rulesManager.init: "+e);}},
		
		//check to see if any rules match the post object
		doEvent:function(event,obj){
			//do nothing if disabled
			if (!WM.rulesManager.enabled) return;
			
			//log("WM.rulesManager.doEvent: event="+event+", post="+post.id);
			for (var r=0,rule;(rule=WM.rulesManager.rules[r]);r++){
				if (rule.enabled) (function(){rule.doEvent(event,obj);})();
			}
		},
		
		//convert a test (such as dynamic grab entry) to a rule
		ruleFromTest:function(test){
			//[{"id":"_h6qil21n","label":"new test","search":["body"],"find":["nothing"],"ret":"dynamic","kids":[{"id":"_h6qiw4zf","find":[]}],"appID":"102452128776","disabled":true}]
			//[{"enabled":true,"limit":0,"limitCount":0,"expanded":true,"validators":[{"search":["body"],"operand":"lessThan","find":"chipmunk"}],"actions":[{"event":"onIdentify","action":"setColor","params":"orange"}],"kids":[{"enabled":true,"limit":0,"limitCount":0,"expanded":true,"validators":[],"actions":[],"kids":[],"eggs":[]},{"enabled":true,"limit":0,"limitCount":0,"expanded":true,"validators":[],"actions":[],"kids":[],"eggs":[]}],"eggs":[{"enabled":true,"limit":0,"limitCount":0,"expanded":true,"validators":[],"actions":[],"kids":[],"eggs":[]},{"enabled":true,"limit":0,"limitCount":0,"expanded":true,"validators":[],"actions":[],"kids":[],"eggs":[]}]}]
			var ret={
				title:(test.label||test.title)||"Converted Dynamic Test",
				enabled:!(test.disabled||false),
				limit:0,
				limitCount:0,
				expanded:true,
				validators:function(){
					var ret=[];
					//add the initial validator
					ret.push({
						search:["appID"],
						operand:"equals",
						find:test.appID
					});
					//detect search/find method
					var method="basic";
					if (isArrayAndNotEmpty(test.subTests) && test.find.contains("{%1}")) method="subTests";
					if (exists(test.subNumRange) && test.find.contains("{%1}")) method="subNumRange";
					if (test.regex==true) method="regexp";
					if (method=="regexp") {
						//leave the expression just as it is
						ret.push({
							search:test.search||[],
							operand:"matchRegExp",
							find:test.find,
						});
					} else if (method=="basic") {
						//convert the test.find array into a regular espression
						ret.push({
							search:test.search||[],
							operand:"matchRegExp",
							find:arrayToRegExp(test.find),
						});
					} else if (method=="subTests") {
						//insert the subTests into the find insertion point as a regular expression
						//but make the rest of the find parameter not return if found
						var find=test.find;
						if (find.contains("{%1}")){
							find=find.split("{%1}");
							find=(find[0].length?("(?:"+find[0]+")"):"")+arrayToRegExp(test.subTests)+(find[1].length?("(?:"+find[1]+")"):"");
						}
						ret.push({
							search:test.search||[],
							operand:"matchRegExp",
							find:find
						});
					} else if (method=="subNumRange") {
						//insert the subNumRange into the find insertion point as a regular expression
						//but make the rest of the find parameter not return if found
						var numRange=("string"==typeof test.subNumRange)?test.subNumRange.split(","):[test.subNumRange.low,test.subNumRange.high];
						var find=test.find;
						if (find.contains("{%1}")){
							find=find.split("{%1}");
							find=(find[0].length?("(?:"+find[0]+")"):"")+integerRangeToRegExp({min:numRange[0],max:numRange[1]})+(find[1].length?("(?:"+find[1]+")"):"");
						}
						ret.push({
							search:test.search||[],
							operand:"matchRegExp",
							find:find
						});
					}
					return ret;
				}(),
				actions:[
					{
						event:"onIdentify",
						action:"setWhich",
						params:test.ret||"dynamic",
					}
				],
				kids:[],
				eggs:[],				
			};
			
			//convert children
			if (isArrayAndNotEmpty(test.kids)) {
				for (var k=0,kid;(kid=test.kids[k]);k++) {
					ret.kids.push(WM.rulesManager.ruleFromTest(kid));
				}
			}
			
			return ret;
		},
		
		//create a rule based on a specific post
		ruleFromPost:function(post){
			//create some data to get us started
			var rule={
				basedOn:post,
				title:"Based On: "+post.id,
				enabled:false, //start out not using this rule
				validators:[
					{search:["appID"],find:post.appID,operand:"equals"},
					{search:["title"],find:post.name,operand:"matchRegExp"},
					{search:["caption"],find:post.caption,operand:"matchRegExp"},
					{search:["desc"],find:post.description,operand:"matchRegExp"},
					{search:["link"],find:post.linkText,operand:"matchRegExp"},
				],
				actions:[
					{event:"onIdentify",action:"setWhich",params:"dynamic"}
				]
			};
			WM.rulesManager.rules.push(rule=new WM.rulesManager.Rule(rule));

			if (WM.opts.rulesJumpToNewRule){
				//jump to rule view
				WM.console.tabContainer.selectTab(3);
				//scroll to new rule
				rule.node.scrollIntoView();
			}
		},
		
		//copy all dynamics to new rules
		//does not destroy dynamics as they are converted
		convertDynamics:function(){
			var tests=WM.grabber.tests;
			if (isArrayAndNotEmpty(tests)) {
				for (var t=0,test;(test=tests[t]);t++){
					WM.rulesManager.rules.push( new WM.rulesManager.Rule( WM.rulesManager.ruleFromTest(test) ) );
				}
			}
		},

		//rest rule limits for all rules and their children
		resetAllLimits:function(params){
			params=params||{};
			var ask=WM.opts.rulesConfirmResetLimit;
			if (params.noConfirm || !ask || (ask && confirm("Reset Limit Counter?"))) {
				if (isArrayAndNotEmpty(WM.rulesManager.rules)) for (var r=0,rule;(rule=WM.rulesManager.rules[r]);r++) {
					rule.resetLimit({preventSave:true,resetChildren:true,noConfirm:true});
				}
				WM.rulesManager.saveRules();
			}
		},
		
		saveRules : function(){try{
			//pack rule objects
			var retRules=[];
			var retGlobal={};
			
			if (isArrayAndNotEmpty(WM.rulesManager.rules)) {
				for (var r=0,rule; (rule=WM.rulesManager.rules[r]);r++){
					if (!rule.isGlobal) {
						retRules.push(rule.saveableData);
					} else {
						//make a placeholder locally
						retRules.push({isGlobal:true, uniqueID:rule.uniqueID, enabled:rule.enabled, expanded:rule.expanded});
						//and save it globally
						var glob=rule.saveableData;
						glob.uniqueID=rule.uniqueID;
						retGlobal[rule.uniqueID]=glob;						
					}
				}
			}
			
			//save rules
			setOptJSON("priority3_"+WM.currentUser.profile,retRules);
			setOptJSON("priority3_global",retGlobal);
		}catch(e){log("WM.rulesManager.saveRules: "+e);}},	
		
		showData : function(){try{
			promptText(getOpt("priority3_"+WM.currentUser.profile),true);
		}catch(e){log("WM.rulesManager.showData: "+e);}},	

		newRule : function(p){try{
			var rule=new WM.rulesManager.Rule(p);
			WM.rulesManager.rules.push(rule);
			WM.rulesManager.saveRules(); 
		}catch(e){log("WM.rulesManager.newRule: "+e);}},

		importRule: function(){try{
			var params=prompt("Input rule data",null);
			if (params) {
				var convertedInput=JSON.parse(params);
				if (isArray(convertedInput)){
					for (var i=0;i<convertedInput.length;i++){
						WM.rulesManager.newRule(convertedInput[i]);
					}
				} else {
					WM.rulesManager.newRule(convertedInput);
				}
			}
		}catch(e){log("WM.rulesManager.importRule: "+e);}},

		toggleHeartbeat: function(){try{
			WM.quickOpts.heartbeatDisabled=!WM.quickOpts.heartbeatDisabled;
			with (WM.rulesManager.toggleHBNode) {
				className=className.swapWordB(WM.quickOpts.heartbeatDisabled,"oddOrange","oddGreen");
			}
			WM.saveQuickOpts();
			log(WM.quickOpts.heartbeatDisabled);
		}catch(e){log("WM.rulesManager.toggleHeartbeat: "+e);}},
	};
	
//***************************************************************************************************************************************
//***** Rules Manager Enums & Functions
//***************************************************************************************************************************************
	WM.rulesManager.ruleActions = {
		"addToFeeds":{name:"Add Poster To Feeds",toolTip:"Add the post's creator to your feeds manager. Can also be used with onFriend* events."}, 
		"appendLink":{name:"Append To Link",toolTip:"Add specific code to the end of the collection link.",hasParam:true,paramType:"textBox","default":""},
		"birth":{name:"Birth Eggs",toolTip:"Clone the egg children to this rule's level, without destroying this rule."}, 
		"cancelInterval":{name:"Cancel Interval",toolTip:"Destroy the repeating timer on this rule."},
		"cancelTimer":{name:"Cancel Timer",toolTip:"Destroy the timer on this rule."} ,
		"cleanPost":{name:"Clean Post",toolTip:"Remove the calling post from the collector."}, 
		"commentPost":{name:"Comment Post",toolTip:"Create a comment on the calling post.",hasParam:true,paramLabel:"comment",paramType:"string","default":"Thanks!"}, 
		"createInterval":{name:"Create Interval",toolTip:"Create a repeating timer on this rule, where 1000 equals 1 second.",hasParam:true,paramType:"timePicker","default":1000} ,
		"createTimer":{name:"Create Timer",toolTip:"Create a timer on this rule, where 1000 equals 1 second.",hasParam:true,paramType:"timePicker","default":1000},
		"decrementCounter":{name:"Decrement Limit Counter",toolTip:"Decrement the rule limit counter.",hasParam:true,paramType:"number","default":1},
		"decrementParentCounter":{name:"Decrement Parent Limit Counter",toolTip:"Decrement the parent rule limit counter.",hasParam:true,paramType:"number","default":1},
		"destroyRule":{name:"Destroy Rule",toolTip:"Permanently removes this rule and all of its children."},
		"disableApp":{name:"Disable App",toolTip:"Disable the specified app. Leave blank to disable the app associated with the activating post.",hasParam:true,paramType:"textBox","default":""},
		"disableAppOption":{name:"Disable App Option",toolTip:"Disable an option in the related sidekick by internal name.",hasParam:true,paramType:"textBox","default":""},
		"disableAutocomment":{name:"Disable Autocomment",toolTip:"Disable the autocomment feature."},
		"disableAutolike":{name:"Disable Autolike",toolTip:"Disable the autolike feature."},
		"disableChildRules":{name:"Disable Child Rules",toolTip:"Disable the immediate children of this rule. Does not disable this rule."},
		"disableHostOption":{name:"Disable Host Option",toolTip:"Disable an option in the wm host by internal name.",hasParam:true},
		"disableRule":{name:"Disable Rule",toolTip:"Disable the current rule."}, 
		"emergencyOpen":{name:"Emergency Open",toolTip:"Move the calling post directly to a new processing window, no matter what your opened window limit is."}, 
		"emptyAutolikeQueue":{name:"emptyAutolikeQueue",toolTip:"Destroys the list of posts you intended to autolike or autocomment."}, 
		"enableApp":{name:"Enable App",toolTip:"Enable the specified app. Leave blank to enable the app associated with the activating post.",hasParam:true,paramType:"textBox","default":""},
		"enableAppOption":{name:"Enable App Option",toolTip:"Enable an option in the related sidekick by internal name.",hasParam:true,paramType:"textBox","default":""},
		"enableAutocomment":{name:"Enable Autocomment",toolTip:"Enable the autocomment feature."},
		"enableAutolike":{name:"Enable Autolike",toolTip:"Enable the autolike feature."},
		"enableChildRules":{name:"Enable Child Rules",toolTip:"Enable the immediate children of this rule."}, 
		"enableHostOption":{name:"Enable Host Option",toolTip:"Enable an option in the wm host by internal name.",hasParam:true},
		"enableRule":{name:"Enable Rule",toolTip:"Enable the current rule."}, 
		"fetchNewer":{name:"Fetch Newer Posts",toolTip:"Fetch some more posts for this app, feed or feed filter."}, 
		"fetchOlder":{name:"Fetch Older Posts",toolTip:"Fetch some more posts for this app, feed or feed filter."}, 
		"fetchHours":{name:"Fetch Hours of Posts",toolTip:"Fetch some more posts for this app, feed or feed filter.",hasParam:true,paramType:"number","default":24}, 
		"forceOpen":{name:"Force Open",toolTip:"Move the calling post directly to the collector queue."}, 
		"forceOpenFirst":{name:"Force Open First",toolTip:"Move the calling post directly to the collector queue AND cut in line to be next processed."}, 
		"hatch":{name:"Hatch Eggs",toolTip:"Hatch the egg-children of the current rule, and destroy this rule."}, 
		"incrementCounter":{name:"Increment Limit Counter",toolTip:"Increment the rule limit counter.",hasParam:true,paramType:"number","default":1},
		"incrementParentCounter":{name:"Increment Parent Limit Counter",toolTip:"Increment the parent rule limit counter.",hasParam:true,paramType:"number","default":1},
		"likePost":{name:"Like Post",toolTip:"Like the calling post."}, 
		"openPostSource":{name:"Open Post Source",toolTip:"Opens the post source in a separate window/tab."},
		"processLast":{name:"Move To Bottom",toolTip:"Move the post to the bottom of the collector window."}, 
		"processFirst":{name:"Move To Top",toolTip:"Move the post to the top of the collector window."}, 
		"pauseAllApps":{name:"Pause All Apps",toolTip:"Pause all apps currently associated with docked sidekicks."}, 
		"pauseApp":{name:"Pause App",toolTip:"Pauses processing anything by this app.",hasParam:true,paramType:"textBox","default":""}, 
		"pauseWM.collector":{name:"Pause WM.collector",toolTip:"Pauses collection of all posts."}, 
		"pauseFetch":{name:"Pause Fetching",toolTip:"Pauses fetching of all posts."}, 
		"pauseType":{name:"Pause Type",toolTip:"Pause collection of all bonuses of this type."}, 
		"pinPost":{name:"Pin Post",toolTip:"Pins the calling post."}, //pin the post
		"queueCommentPost":{name:"Queue Comment Post",toolTip:"Comment on the calling post by first using the autolike queue system to delay the autocomment.",hasParam:true,paramLabel:"comment",paramType:"string","default":"Thanks!"},
		"queueLikePost":{name:"Queue Like Post",toolTip:"Like the calling post by first using the autolike queue system to delay the autolike."},
		"refreshBrowser":{name:"Refresh Browser",toolTip:"Reloads the browser window."}, 
		"reIDAll":{name:"ReID All",toolTip:"Re-ID all posts in the collector."},
		"removePriority":{name:"Remove Priority",toolTip:"Sets the priority of the calling post to normal."}, 
		"removePriorityApp":{name:"Remove Priority (App)",toolTip:"Sets the priority of all posts of the calling or specified app to normal.",hasParam:true,paramType:"textBox","default":""}, 
		"removePriorityType":{name:"Remove Priority (Type)",toolTip:"Sets the priority of all posts of the calling app with specified or associated type to normal.",hasParam:true,paramType:"textBox","default":"dynamic"}, 
		"resetAllLimits":{name:"Reset All Limit Counters",toolTip:"Reset all limits in the rules manager."},
		"resetLimit":{name:"Reset Limit Counter",toolTip:"Reset the limit counter of the current rule."},
		"resetBranchLimits":{name:"Reset Branch Limit Counters",toolTip:"Reset the limit counter of ALL rules that are lower in this branch (children, grandchildren, etc.). Does not reset the limit on this rule."},
		"resetChildrenLimits":{name:"Reset Children Limit Counters",toolTip:"Reset the limit counter of immediate child rules of this rule. Does not reset the limit on this rule."},
		"resetParentLimit":{name:"Reset Parent Limit Counter",toolTip:"Reset the limit counter of the parent rule."},
		"setAppOption":{name:"Set App Option",toolTip:"Set an option in the related sidekick by internal name.",hasParam:true,paramCount:2,paramData:[{paramType:"textBox","default":"",paramLabel:"Name"},{paramType:"textBox","default":"",paramLabel:"Value"}]},
		"setAppTab":{name:"Set App Tab",toolTip:"Set the current collection tab by app ID.",hasParam:true,paramType:"textBox","default":"all"},
		"setAsAccepted":{name:"Set As Accepted",toolTip:"Set the calling post as accepted.",hasParam:true,paramType:"checkBox",paramLabel:"saveToHistory","default":false},
		"setAsExcluded":{name:"Set As Excluded",toolTip:"Set the calling post as excluded."}, 
		"setAsFailed":{name:"Set As Failed",toolTip:"Set the calling post as failed.",hasParam:true,paramType:"checkBox",paramLabel:"saveToHistory","default":false},
		"setColor":{name:"Set Post Color",toolTip:"Set the background color of the calling post.",hasParam:true,paramType:"colorPicker","default":"blue"},
		"setHostOption":{name:"Set Host Option",toolTip:"Set the value a host option by internal name.",hasParam:true,paramCount:2,paramData:[{paramType:"textBox","default":"",paramLabel:"Name"},{paramType:"textBox","default":"",paramLabel:"Value"}]},
		"setPriority":{name:"Set Priority",toolTip:"Set the priority of the calling post.",hasParam:true,paramType:"number","default":50}, 
		"setPriorityApp":{name:"Set Priority (App)",toolTip:"Set the priority of the calling app or specified app.",hasParam:true,paramCount:2,paramData:[{paramType:"textBox",paramLabel:"App","default":""},{paramType:"number",paramLabel:"Priority","default":50}]}, 
		"setPriorityType":{name:"Set Priority (Type)",toolTip:"Set the priority of the calling post type or specified type for the same app.",hasParam:true,paramCount:2,paramData:[{paramType:"textBox",paramLabel:"Type Code","default":""},{paramType:"number",paramLabel:"Priority","default":50}]}, 
		"setToCollect":{name:"Set To Collect",toolTip:"Set the calling post to be collected in normal order. Use Force Open to do more immediate collection, or Emergency Open to override your opened window limit."},
		"setToCollectPriority1":{name:"Set To Collect Top Priority",toolTip:"Set the calling post to be collected and also set its priority to 1. Use Force Open to do more immediate collection, or Emergency Open to override your opened window limit."},
		"setWhich":{name:"Set Type",toolTip:"Set the bonus type id of the calling post.",hasParam:true,paramType:"textBox","default":"dynamic"},	
		"uncheckType":{name:"Uncheck Post Type",toolTip:"Unchecks option to collect this bonus in the options menu."},		
		"unpauseAllApps":{name:"Unpause All Apps",toolTip:"Unpause all apps currently associated with docked sidekicks."}, 
		"unpauseAllTypesAllApps":{name:"Unpause All Types",toolTip:"Unpause all bonus types by all apps."}, 
		"unpauseAllTypesByApp":{name:"Unpause All Types By App",toolTip:"Unpause all bonus types associated with the given app, or the app associated with the activating post.",hasParam:true,paramType:"textBox","default":""}, 
		"unpauseApp":{name:"Unpause App",toolTip:"Starts processing anything by this app.",hasParam:true,paramType:"textBox","default":""},		
		"unpauseWM.collector":{name:"Unpause WM.collector",toolTip:"Starts collection of posts."}, 
		"unpauseFetch":{name:"Unpause Fetching",toolTip:"Starts fetching of posts."}, 
		"unpauseType":{name:"Unpause Type",toolTip:"Unpause collection of all bonuses of this type."}, 
	};
	
	WM.rulesManager.ruleActionsCodes = {
		"addToFeeds":1,"appendLink":2,"birth":3,"cancelInterval":4,"cancelTimer":5,"cleanPost":6,"commentPost":7,"createInterval":8,"createTimer":9,
		"decrementCounter":10,"decrementParentCounter":11,"destroyRule":12,"disableApp":13,"disableAppOption":14,"disableAutolike":15,"disableChildRules":16,
		"disableHostOption":17,"disableRule":18,"emergencyOpen":19,"emptyAutolikeQueue":20,"enableApp":21,"enableAppOption":22,"enableAutolike":23,
		"enableChildRules":24,"enableHostOption":25,"enableRule":26,"fetchNewer":27,"fetchOlder":28,"forceOpen":29,"forceOpenFirst":30,"hatch":31,
		"incrementCounter":32,"incrementParentCounter":33,"likePost":34,"openPostSource":35,"processLast":36,"processFirst":37,"pauseAllApps":38,
		"pauseApp":39,"pauseWM.collector":40,"pauseFetch":41,"pauseType":42,"pinPost":43,"queueCommentPost":44,"queueLikePost":45,"refreshBrowser":46,
		"reIDAll":47,"removePriority":48,"removePriorityApp":49,"removePriorityType":50,"resetAllLimits":51,"resetLimit":52,"resetBranchLimits":53,
		"resetChildrenLimits":54,"resetParentLimit":55,"setAppOption":56,"setAppTab":57,"setAsAccepted":58,"setAsExcluded":59,"setAsFailed":60,"setColor":61,
		"setHostOption":62,"setPriority":63,"setPriorityApp":64,"setPriorityType":65,"setToCollect":66,"setToCollectPriority1":67,"setWhich":68,
		"uncheckType":69,"unpauseAllApps":70,"unpauseAllTypesAllApps":71,"unpauseAllTypesByApp":72,"unpauseApp":73,"unpauseWM.collector":74,"unpauseFetch":75,
		"unpauseType":76,"fetchHours":77,"enableAutocomment":78,"disableAutocomment":79
	};
	
	WM.rulesManager.ruleActionByCode = function(code){
		for (c in WM.rulesManager.ruleActionsCodes) {
			if (WM.rulesManager.ruleActionsCodes[c]==code) return c;
		}
		return null;
	};	
	
	WM.rulesManager.ruleEvents = {
		//post events
		"onIdentify":"Called after a post is (re)identified. Posts are first identified as soon as they are fetched.",
		"onBeforeCollect":"Called before collection opens a sidekick window.",
		"onAfterCollect":"Called after collection is tried. Activates regardless of return status.",
		"onFailed":"Called when a post is marked failed. This could be actual or simulated by the user.",
		"onAccepted":"Called when a post is marked accepted. This could be actual or simulated by the user.",
		"onTimeout":"Called when a post is marked as timed out. This could be actual or simulated by the user.",
		"onValidate":"Called when a post is first fetched, but after its first identification. Not called on posts which fail identification.",
		
		//rule events
		"onLimit":"Called when this rule limit counter equals the rule's limit.",
		"onHatch":"Called when this rule's egg children are hatched.",		
		"onTimer":"Called when the timer on this rule activates.",
		"onInterval":"Called when the repeating timer on this rule activates.",
		"onBirth":"Called when this rule's egg children are birthed.",
		"onRuleCreated":"Called when the rule is created (or loaded on startup).",
		"onRuleButtonClicked":"Called when the rule button is clicked. Available only for control rules.",
		
		//app events
		"onSidekickDock":"Called when the sidekick for this app docks.",
		"onSidekickReady":"Called when the sidekick for this app creates an app object, and after it appends the collection tab for that app.",
		/*
			paused/unpaused
			enabled/disabled
			failCountChanged
			acceptCountChanged
			
		*/
		
		//console events
		"onHeartbeat":"Called when the global heartbeat interval ticks.",
		"onSetAppFilter":"Called when the collection panel app tab changes, including at startup if 'Show All' is selected as default",
		
		//feed events
		"onFeedFilterOlderLimitReached":"Called when a specific feed filter reaches its user-defined older limit.",
		
	};
	
	WM.rulesManager.ruleEventsCodes ={
		"onIdentify":1,"onBeforeCollect":2,"onAfterCollect":3,"onFailed":4,"onAccepted":5,"onTimeout":6,"onValidate":7,"onLimit":8,"onHatch":9,"onTimer":10,
		"onInterval":11,"onBirth":12,"onRuleCreated":13,"onSidekickDock":14,"onSidekickReady":15,"onHeartbeat":16,"onSetAppFilter":17,
		"onFeedFilterOlderLimitReached":18,"onRuleButtonClicked":19
	};
	
	WM.rulesManager.ruleEventByCode = function(code){
		for (c in WM.rulesManager.ruleEventsCodes) {
			if (WM.rulesManager.ruleEventsCodes[c]==code) return c;
		}
		return null;
	};	
	
	WM.rulesManager.postParts = {
		"age":"The time between the current time and the post creation time (in ms).",
		"acceptCount":"An app's accept counter value. Friend objects also have an acceptCount.",
		"activatorType":"Returns the object type of the rule-activating object: app, post, rule, feed, feedfilter or unknown.",
		"alreadyProcessed":"Reports if a post has already created a history entry.",
		"appID":"The appID of the game for which a post belongs. You can read the appID from the following affected objects: app, post, and feedFilter.",
		"appName":"The appName of the game for which this post belongs, as reported by the FB database.",
		"body":"The body of a post is a compilation of the title, caption, and desc.",
		"canvas":"The canvas of a post is its namespace granted by FB, ie. FarmVille's namespace is 'onthefarm'.",
		"caption":"The caption of a post is one line just below its title (or 'name'). Not all posts have this field.",
		"commentorID":"The commentorID is a list of IDs of all commentors.",
		"commentorName":"The commentorName is a list of names of all commentors.",
		"comments":"The comments are list of all comments made to the post, excluding the initial msg.",
		"currentTime":"The current time (in ms) on your system, not localized. This global value can be referenced from any activating object type.",
		"currentAppTab":"The currently selected collection tab's appID, or the word 'all' if the 'Show All' tab is selected.",
		"date":"The date of a post is its creation time on FB, and is the 'created_time' parameter in fb data packets.",
		"desc":"The desc of a post is two lines below the title. This is the 'description' parameter in fb data packets. Not all posts have this field.",
		"either":"The either of a post is the compilation of the link and body.",
		"enabled":"The enabled state of an activating object.",
		"expanded":"The expanded state of an activating object.",
		"failCount":"An app's fail counter value. Friend objects also have a failCount.",
		"friendAcceptedCount":"Gets the accepted count from a FriendTracker friend object matching this post creator.",
		"friendFailedCount":"Gets the failed count from a FriendTracker friend object matching this post creator.",		
		"fromID":"The fromID is the ID of the poster.",
		"fromName":"The fromName is the name of the poster.",
		"fromNameLastFirst":"The name of the poster, displayed as Lastname, Firstname",
		"html":"The html of a post is the compilation of ALL visible FB attributes.",		
		"id":"Normally a post ID, which is usually the post creator's ID and a timestamp separated by an underscore. Alternately, you can ask for the id of an activating friend, feed or feed filter object.",
		"idText":"The identified link text of a post.",
		"img":"The img of a post is the url of the icon that displays with the post. This is the 'picture' parameter in fb data packets.",
		"isAccepted":"Reports if the post is set as having already been successfully collected.",
		"isAppPaused":"Reports if the associated app is paused.",
		"isCollect":"Reports if the post is set to be collected.",
		"isExcluded":"Reports if the post has been set as excluded.",
		"isFailed":"Reports if the post is set as having already failed.",
		"isForMe":"Reports if the W2W post targets the current user.",
		"isLiked":"Reports if the post has been identified as already being liked by the current user.",
		"isMyPost":"Reports if the post belongs to the current user.",
		"isPaused":"Reports if the calling object (post or app) is paused. Not specific!",
		"isPinned":"Reports if the post is marked as being pinned.",
		"isRemovable":"Reports if a feed is removeable. Your own profile wall and your home feed are not removeable, only disableable.",
		"isTimeout":"Reports if the post has been marked as a timed out collection attempt.",
		"isTypePaused":"Reports if the associated bonus type is paused.",
		"isScam":"Reports if a post is suspected of being a scam, usually when the canvas and appName do not match.",
		"isStale":"Reports if a post is older than the user-set older limit.",
		"isUndefined":"Reports if the post does not match any id given by the sidekick.",
		"isWishlist":"Reports if the post is deemed a whichlist request.",
		"isWorking":"Reports if the post is currently in the working state (being processed).",
		"isW2W":"Reports if the post is a Wall-To-Wall post, meaning that it was posted to a specific user's wall.",
		"lastKnownPostDate":"A friend object's last known post date (as unix time, no millisecond data).",
		"likeID":"The likeID is a list of IDs of users who liked the post.",
		"likeName":"The likeName is a list of names of users who liked this post.",
		"limit":"This rule's limit number.",
		"limitCount":"This rule's limit counter.",
		"link":"The 'link' of a post is the link text, not the url. This is the 'action.name' in fb data packets.",
		"linkHref":"The original url as it appeared from facebook. This SHOULD be exactly the same as 'url'.",
		"linkText":"The original link text as it appeared from facebook. You may want to NOT use 'link' and instead use this one.",
		"msg":"The msg of a post is the part the poster added as a comment during the post's creation.",
		"name":"With posts, this is the same as 'title', because its the FB name of a post object. With friend objects, this is the friend's text name.",
		"parentLimit":"The parent rule's limit number, or NULL if no parent exists.",
		"parentLimitCount":"The parent rule's limit counter, or NULL if no parent exists.",
		"postCount":"A friend object's count of posts it is tracking.",
		"postedDay":"A partial date-time value containing only the year/month/day portions, which corresponds to the post creation time.",
		"postedHour":"A partial date-time value containing only the year/month/day/hour portions, which corresponds to the post creation time.",
		"priority":"The priority of a post which could have been set by a rule, or by default of 50.",
		"status":"The status of a post is the return code given by a sidekick, or 0 if it has not been processed.",
		"targetID":"The targetID is a list of targets' IDs that the poster intended the post to display to.",
		"targetName":"The targetName is a list of targets the poster intended the post to display to.",
		"title":"The title of a post contains the bold text, usually including the poster's name, at the top of the post. This is the 'name' parameter in facebook data packets.",
		"totalCount":"An app's failcount and acceptcount combined. Friend objects also have a totalCount.",
		"typesPaused":"An app's list of paused bonus types. Only accessible from an activating post. Please stick to the contains/notContains operators because this is an array, not text.",
		"url":"The url of a post is the address to which the post redirects the user when clicked. This is the 'link' or 'action.link' parameter in fb data packets. This is the original url supplied by the app, not a modified url, such as WM's removal of https or a sidekick-modified url. Alternately, you can ask for the URL of a feed object.",
		"which":"The 'which' of a post is its identified codename that defines its bonus type and ties it to option menu entries. The codename starts with an appID and ends with something the sidekick developer uses to key the bonus type.",
		"whichText":"Text associated with this bonus type.",
	};
	
	WM.rulesManager.postPartsCodes = {
		"age":1,"acceptCount":2,"activatorType":3,"alreadyProcessed":4,"appID":5,"appName":6,"body":7,"canvas":8,"caption":9,"commentorID":10,
		"commentorName":11,"comments":12,"currentTime":13,"currentAppTab":14,"date":15,"desc":16,"either":17,"enabled":18,"expanded":19,"failCount":20,
		"fromID":21,"fromName":22,"fromNameLastFirst":23,"html":24,"id":25,"idText":26,"img":27,"isAccepted":28,"isAppPaused":29,"isCollect":30,
		"isExcluded":31,"isFailed":32,"isForMe":33,"isLiked":34,"isMyPost":35,"isPaused":36,"isPinned":37,"isRemovable":38,"isTimeout":39,"isTypePaused":40,
		"isScam":41,"isStale":42,"isUndefined":43,"isWishlist":44,"isWorking":45,"isW2W":46,"lastKnownPostDate":47,"likeID":48,"likeName":49,"limit":50,
		"limitCount":51,"link":52,"linkHref":53,"linkText":54,"msg":55,"name":56,"parentLimit":57,"parentLimitCount":58,"postCount":59,"postedDay":60,
		"postedHour":61,"priority":62,"status":63,"targetID":64,"targetName":65,"title":66,"totalCount":67,"typesPaused":68,"url":69,"which":70,
		"whichText":71,"friendAcceptedCount":72,"friendFailedCount":73
	};
	
	WM.rulesManager.postPartByCode = function(code){
		for (c in WM.rulesManager.postPartsCodes) {
			if (WM.rulesManager.postPartsCodes[c]==code) return c;
		}
		return null;
	};
	
	WM.rulesManager.ruleOperands = {
		"equals":"Property and query must match.",
		"notEquals":"Property and query must not match.",
		"startsWith":"Property must start with query value.",
		"notStartsWith":"Property cannot start with query value.",
		"endsWith":"Property must end with query value.",
		"notEndsWith":"Property cannot end with query value.",
		"contains":"Property contains anywhere the query value.",
		"notContains":"Property does not contain the query value.",
		"matchRegExp":"Property must match the registered expression.", 
		"notMatchRegExp":"Property must not match the registered expression.",
		"greaterThan":"Property must be greater than query value.",
		"lessThan":"Property must be less than query value.",
		"greaterThanOrEquals":"Property must be greater than or equal to query value.",
		"lessThanOrEquals":"Property must be less than or equal to query value.",

		"equalsExactly":"Property and query must match exactly via binary comparison.",
		"notEqualsExactly":"Property and query must not match exactly via binary comparison.",
		
	};

	WM.rulesManager.ruleOperandsCodes = {
		"equals":1,
		"notEquals":2,
		"startsWith":3,
		"notStartsWith":4,
		"endsWith":5,
		"notEndsWith":6,
		"contains":7,
		"notContains":8,
		"matchRegExp":9, 
		"notMatchRegExp":10,
		"greaterThan":11,
		"lessThan":12,
		"greaterThanOrEquals":13,
		"lessThanOrEquals":14,
		"equalsExactly":15,
		"notEqualsExactly":16,
		
	};	
	
	WM.rulesManager.ruleOperandByCode = function(code){
		for (c in WM.rulesManager.ruleOperandsCodes) {
			if (WM.rulesManager.ruleOperandsCodes[c]==code) return c;
		}
		return null;
	};	
	
//***************************************************************************************************************************************
//***** RuleValidator Class
//***************************************************************************************************************************************
	WM.rulesManager.RuleValidator = function(params){try{
		var isNew=(!exists(params));
		var self=this;
	
		//return saveable data from this branch
		this.__defineGetter__("saveableData",function(){try{
			var s=self.search, modSearch=[]; //use a second array to avoid accidental overwrite of first byRef
			for (var c=0;c<s.length;c++){
				modSearch.push(WM.rulesManager.postPartsCodes[s[c]]);
			}
			var ret = {search:modSearch, operand:WM.rulesManager.ruleOperandsCodes[self.operand], find:self.find}
			return ret;
		}catch(e){log("WM.rulesManager.RuleValidator.saveableData: "+e);}});

		//remove this from parent
		this.remove=function(){try{
			var ask=WM.opts.rulesConfirmDeleteValidator;
			if (!ask || (ask && confirm("Delete rule validator?"))){
				remove(this.node);
				this.parent.validators.removeByValue(this);
				doAction(WM.rulesManager.saveRules);
			}
		}catch(e){log("WM.rulesManager.RuleValidator.remove: "+e);}};
	
		this.moveUp=function(){try{
			//where is this
			var parentContainer = this.parent.validators;
			//only affects items not already the first in the list
			//and not the only child in the list
			if ((parentContainer.length>1) && (parentContainer[0]!=this)) {
				//which index is this?
				var myIndex=parentContainer.inArrayWhere(this);
				if (myIndex != -1) {
					//I have a proper index here
					//who is my sibling
					var sibling = parentContainer[myIndex-1];
					//swap me with my sibling
					parentContainer[myIndex-1]=this;
					parentContainer[myIndex]=sibling;
					//place my node before my sibling node
					sibling.node.parentNode.insertBefore(this.node,sibling.node);
					//save it
					WM.rulesManager.saveRules();
				}
			}
		}catch(e){log("WM.rulesManager.RuleValidator.moveUp: "+e);}};

		//move down in the list
		this.moveDown=function(){try{
			//where is this
			var parentContainer = this.parent.validators;
			//only affects items not already the first in the list
			//and not the only child in the list
			if ((parentContainer.length>1) && (parentContainer.last()!=this)) {
				//which index is this?
				var myIndex=parentContainer.inArrayWhere(this);
				if (myIndex != -1) {
					//I have a proper index here
					//who is my sibling
					var sibling = parentContainer[myIndex+1];
					//swap me with my sibling
					parentContainer[myIndex+1]=this;
					parentContainer[myIndex]=sibling;
					//place my node before my sibling node
					sibling.node.parentNode.insertBefore(sibling.node,this.node);
					//save it
					WM.rulesManager.saveRules();
				}
			}
		}catch(e){log("WM.rulesManager.RuleValidator.moveDown: "+e);}};

		//copy this validator on the parent
		this.clone=function(){try{
			this.parent.addValidator({search:this.search, operand:this.operand, find:this.find});
			WM.rulesManager.saveRules();
		}catch(e){log("WM.rulesManager.RuleValidator.clone: "+e);}};

		//init
		//this.id=params.id||unique();
		this.parent=params.parent||null;
		if (!this.parent) {
			log("WM.rulesManager.RuleValidator: no parent specified: abort init");
			return null;
		}
		//this.validationNode=parent.validationNode;
		this.search=params.search||["appID"];
		if (!isArray(this.search)) this.search=[].push(this.search);
		//convert number codes to text commands
		for (var e in this.search) {
			//t=this.search[e];
			if (isNumber(this.search[e])) this.search[e]=WM.rulesManager.postPartByCode(this.search[e]);
			//log([this.search[e],t])
		}
		this.operand=params.operand||"matchRegExp";
		if (isNumber(this.operand)) this.operand=WM.rulesManager.ruleOperandByCode(this.operand);
		this.find=params.find||"";
				
		//draw it
		this.parent.validationNode.appendChild(this.node=createElement("div",{className:"validator"},[
			//search portion for this validator
			createElement("div",{className:"line"},[
				this.searchNode=(this.objSearch=new jsForms.comboBox({
					className:"jsfComboBox selectPostPart",
					onChange:function(){
						self.search=this.value;
						WM.rulesManager.saveRules();
					},
					items:(function(){
						var ret=[];
						for (var i in WM.rulesManager.postParts){
							ret.push(new jsForms.checkBox({
								text:i,
								value:i,
								toolTipText:WM.rulesManager.postParts[i],
								checked:(self.search.inArray(i)),
								size:{width:"200%"},
							}));
						}
						return ret;
					})(),
					borderStyle:"none",
					//borderRadius:{topLeft:"1px", bottomRight:"1px",topRight:"1px",bottomLeft:"1px"},
					//explicitClose:true,
					highlightSelected:true,					
					dropDownSize:{height:"200px"},
					backColor:"#EEEEEE",
				})).node,
							
				//operator portion for this validator
				this.operandNode=createElement("select",{className:"selectOperand",onchange:function(){self.operand=this.value;WM.rulesManager.saveRules();}},(function(){
					var ret=[],elem;
					for (var i in WM.rulesManager.ruleOperands){
						ret.push(elem=createElement("option",{textContent:i,value:i,title:WM.rulesManager.ruleOperands[i]}));
						if (i==self.operand) elem.selected=true;
					}
					return ret;
				})()),
				//find portion for this validator
				/*
					right here we need to bring up an element based on
						the post part chosen
					for most cases, we just need an input box to accept string values
					for special case "which" we need a dropdown of bonus types
					for boolean flags we need a default value of true and maybe 
						some kind of limitation to true and false in the box
				*/
				this.findNode=createElement("input",{className:"findBox",value:this.find,onchange:function(){self.find=this.value;WM.rulesManager.saveRules();}}),
				//toolbox
				createElement("div",{className:"littleButton oddOrange",onclick:function(){self.remove();},title:"Delete Validator"},[
					createElement("img",{className:"resourceIcon trash"+WM.opts.littleButtonSize}),
				]),
				createElement("div",{className:"littleButton oddBlue",onclick:function(){self.clone();},title:"Clone Validator"},[
					createElement("img",{className:"resourceIcon clone"+WM.opts.littleButtonSize}),
				]),
				createElement("div",{className:"littleButton oddGreen",onclick:function(){self.moveUp();},title:"Move Up"},[
					createElement("img",{className:"resourceIcon arrowUp"+WM.opts.littleButtonSize}),
				]),
				createElement("div",{className:"littleButton oddOrange",onclick:function(){self.moveDown();},title:"Move Down"},[
					createElement("img",{className:"resourceIcon arrowDown"+WM.opts.littleButtonSize}),
				]),
				(self.parent.basedOn)?createElement("div",{className:"indent littleButton oddBlue",onclick:function(){
					//if a validator search array exists
					if (isArrayAndNotEmpty(self.search)){
						//fill the 'find' box with the post data linked to the search terms
						var f="";
						var post=self.parent.basedOn;
						for (var s=0;s<self.search.length;s++){
							if (s>0) f+=" ";
							f+=(post.testData[self.search[s]]||post[self.search[s]]||"");
						}
						self.findNode.value=f;
						self.find=f;
						WM.rulesManager.saveRules();
					}
				},title:"Capture Text From Linked Post"},[
					createElement("img",{className:"resourceIcon importData"+WM.opts.littleButtonSize}),
				]):null,
			]),
		]));
		
		//if (isNew) WM.rulesManager.saveRules();
		return self;
	}catch(e){log("WM.rulesManager.RuleValidator.init(): "+e);}};

//***************************************************************************************************************************************
//***** RuleAction Class
//***************************************************************************************************************************************
	WM.rulesManager.RuleAction = function(params){try{
		var isNew=(!exists(params));
		var self=this;
	
		//return saveable data from this branch
		this.__defineGetter__("saveableData",function(){try{
			var a= {event:WM.rulesManager.ruleEventsCodes[this.event], action:WM.rulesManager.ruleActionsCodes[this.action]};
			if (this.hasParam) a.params=this.params;
			if (this.paramCount==2) a.params2=this.params2;
			return a;
		}catch(e){log("WM.rulesManager.RuleAction.saveableData: "+e);}});

		//remove this from parent
		this.remove=function(){try{
			var ask=WM.opts.rulesConfirmDeleteAction;
			if (!ask || (ask && confirm("Delete rule action?"))){
				remove(this.node);
				this.parent.actions.removeByValue(this);
				doAction(WM.rulesManager.saveRules);
			}
		}catch(e){log("WM.rulesManager.RuleAction.remove: "+e);}};
	
		//move up in the list
		this.moveUp=function(){try{
			//where is this
			var parentContainer = this.parent.actions;
			//only affects items not already the first in the list
			//and not the only child in the list
			if ((parentContainer.length>1) && (parentContainer[0]!=this)) {
				//which index is this?
				var myIndex=parentContainer.inArrayWhere(this);
				if (myIndex != -1) {
					//I have a proper index here
					//who is my sibling
					var sibling = parentContainer[myIndex-1];
					//swap me with my sibling
					parentContainer[myIndex-1]=this;
					parentContainer[myIndex]=sibling;
					//place my node before my sibling node
					sibling.node.parentNode.insertBefore(this.node,sibling.node);
					//save it
					WM.rulesManager.saveRules();
				}
			}
		}catch(e){log("WM.rulesManager.RuleAction.moveUp: "+e);}};

		//move down in the list
		this.moveDown=function(){try{
			//where is this
			var parentContainer = this.parent.actions;
			//only affects items not already the first in the list
			//and not the only child in the list
			if ((parentContainer.length>1) && (parentContainer.last()!=this)) {
				//which index is this?
				var myIndex=parentContainer.inArrayWhere(this);
				if (myIndex != -1) {
					//I have a proper index here
					//who is my sibling
					var sibling = parentContainer[myIndex+1];
					//swap me with my sibling
					parentContainer[myIndex+1]=this;
					parentContainer[myIndex]=sibling;
					//place my node before my sibling node
					sibling.node.parentNode.insertBefore(sibling.node,this.node);
					//save it
					WM.rulesManager.saveRules();
				}
			}
		}catch(e){log("WM.rulesManager.RuleAction.moveDown: "+e);}};

		//copy this validator on the parent
		this.clone=function(){try{		
			this.parent.addAction(this.saveableData());
			WM.rulesManager.saveRules();
		}catch(e){log("WM.rulesManager.RuleAction.clone: "+e);}};

		//init
		//this.id=params.id||unique();
		this.parent=params.parent||null;
		if (!this.parent) {
			log("WM.rulesManager.RuleAction: no parent specified: abort init");
			return null;
		}
		//this.actionsNode=parent.actionsNode;
		this.action=params.action||"incrementCounter";
		//log(this.action);
		if (isNumber(this.action)) this.action=WM.rulesManager.ruleActionByCode(this.action);
		this.event=params.event||"onAccepted";
		if (isNumber(this.event)) this.event=WM.rulesManager.ruleEventByCode(this.event);
		
		//setup default values and param types
		//log(this.action);
		var def=WM.rulesManager.ruleActions[this.action];
		this.hasParam = def.hasParam;
		this.params = params.params||def["default"]||((def.paramData||null)?def.paramData[0]["default"]:"");
		this.params2 = params.params2||((def.paramData||null)?def.paramData[1]["default"]:"");
		this.paramCount = def.paramCount;
		
		//draw it
		this.parent.actionsNode.appendChild(this.node=createElement("div",{className:"action"},[
			//event for this action
			createElement("div",{className:"line"},[
				this.eventNode=createElement("select",{className:"selectEvent",onchange:function(){self.event=this.value; if (self.event=="onRuleButtonClicked") {self.parent.ruleButtonHousingNode.style.display="";} else {self.parent.ruleButtonHousingNode.style.display="none";}; WM.rulesManager.saveRules();}},(function(){
					var actioneventsret=[],elem;
					for (var i in WM.rulesManager.ruleEvents){
						actioneventsret.push(elem=createElement("option",{textContent:i,value:i,title:WM.rulesManager.ruleEvents[i]}));
						if (i==self.event) elem.selected=true;
					}
					return actioneventsret;
				})()),
				//function to call on the event
				this.actionNode=createElement("select",{className:"selectFunction",onchange:function(){
					self.action=this.value;
					WM.rulesManager.saveRules();
					//set the param type
					var action = WM.rulesManager.ruleActions[this.value];
					self.paramNode.style.display=((action.hasParam)?"":"none");
					self.param2Node.style.display=((action.hasParam && (action.paramCount==2))?"":"none");		

				}},(function(){
					var actionfuncsret=[],elem;
					for (var i in WM.rulesManager.ruleActions){
						entry=WM.rulesManager.ruleActions[i];
						actionfuncsret.push(elem=createElement("option",{textContent:entry.name,value:i,title:entry.toolTip}));
						if (i==self.action) elem.selected=true;
					}
					return actionfuncsret;
				})()),
				//this is for special cases only and should be hidden otherwise
				this.paramNode=createElement("input",{className:"paramBox",value:this.params,onchange:function(){self.params=this.value;WM.rulesManager.saveRules();}}),
				this.param2Node=createElement("input",{className:"paramBox",value:this.params2,onchange:function(){self.params2=this.value;WM.rulesManager.saveRules();}}),
				
				//toolbox
				createElement("div",{className:"littleButton oddOrange",onclick:function(){self.remove();},title:"Delete Action"},[
					createElement("img",{className:"resourceIcon trash"+WM.opts.littleButtonSize}),
				]),
				createElement("div",{className:"littleButton oddBlue",onclick:function(){self.clone();},title:"Clone Action"},[
					createElement("img",{className:"resourceIcon clone"+WM.opts.littleButtonSize}),
				]),
				createElement("div",{className:"littleButton oddGreen",onclick:function(){self.moveUp();},title:"Move Up"},[
					createElement("img",{className:"resourceIcon arrowUp"+WM.opts.littleButtonSize}),
				]),
				createElement("div",{className:"littleButton oddOrange",onclick:function(){self.moveDown();},title:"Move Down"},[
					createElement("img",{className:"resourceIcon arrowDown"+WM.opts.littleButtonSize}),
				]),

			]),
		]));	
		
		//hide param node when not used
		self.paramNode.style.display=((self.hasParam)?"":"none");
		self.param2Node.style.display=((self.hasParam && (self.paramCount==2))?"":"none");		

		//if (isNew) WM.rulesManager.saveRules();
		return self;
	}catch(e){log("WM.rulesManager.RuleAction.init(): "+e);}};

//***************************************************************************************************************************************
//***** Rule Class
//***************************************************************************************************************************************
	WM.rulesManager.Rule = function(params){try{
		this.objType="rule";
		var self=this;
		params=params||{};

		//set defaults
		this.parent=null;
		this.enabled=true;
		this.kids=[]; //child nodes
		this.eggs=[]; //hatchable child nodes
		this.actions=[]; //events:actions list
		this.validators=[]; //search:find list
		this.limitCount=0; 
		this.limit=0;
		this.actionsNode=null;
		this.validationNode=null;
		this.node=null;
		this.isChild=false;
		this.isEgg=false;
		this.expanded=true;		
		this.timers={};
		this.intervals={};
		this._isGlobal=false;
		
		//return savable data from this branch
		this.__defineGetter__("saveableData",function(){try{
			var ret={};
			
			//ret.id=this.id;
			ret.title=this.title;
			ret.enabled=this.enabled;
			ret.limit=this.limit;
			ret.limitCount=this.limitCount;
			//ret.level=this.level;
			ret.expanded=this.expanded;
			
			ret.validators=[];
			if (isArrayAndNotEmpty(this.validators)) for (var i=0,validator;(validator=this.validators[i]);i++) {
				ret.validators.push(validator.saveableData);
			}
			
			ret.actions=[];
			if (isArrayAndNotEmpty(this.actions)) for (var i=0,action;(action=this.actions[i]);i++) {
				ret.actions.push(action.saveableData);
			}
			
			ret.kids=[];
			if (isArrayAndNotEmpty(this.kids)) for (var i=0,kid;(kid=this.kids[i]);i++) {
				ret.kids.push(kid.saveableData);
			}
			
			ret.eggs=[];
			if (isArrayAndNotEmpty(this.eggs)) for (var i=0,egg;(egg=this.eggs[i]);i++) {
				ret.eggs.push(egg.saveableData);
			}
			
			return ret;
		}catch(e){log("WM.rulesManager.Rule.saveableData: "+e);}});		

		//set/get wether this rule is saved as global or profile
		this.__defineGetter__("isGlobal",function(){try{
			return self._isGlobal;
		}catch(e){log("WM.rulesManager.Rule.isGlobal: "+e);}});
		this.__defineSetter__("isGlobal",function(v){try{
			//only top level rule can be global
			if (self.parent) {
				confirm("Only top level rule can be set to global.");
				return;
			} 
			
			if (!v) {
				if (!confirm("Disabling profile sharing on this rule will prevent other users on this machine from loading it. Are you sure you wish to make this rule locally available only?")) return;
			}
		
			self._isGlobal=v;
			
			//make sure we have a uniqueID
			//but don't destroy one that already exists
			if (v && !exists(self.uniqueID)) self.uniqueID = unique();
			
			//change the color/icon of the isGlobal button
			if (self.toggleGlobalButton) {
				var s=WM.opts.littleButtonSize;
				with (self.toggleGlobalButton) className=className.swapWordB(v,"removeGlobal"+s,"addGlobal"+s);
				with (self.toggleGlobalButton.parentNode) {
					className=className.swapWordB(v,"oddOrange","oddGreen");
					title=(v)?"Disable Profile Sharing":"Share With Other Profiles";
				}
			}
		}catch(e){log("WM.rulesManager.Rule.isGlobal: "+e);}});
		
		this.__defineGetter__("parentLimit",function(){try{
			if (self.parent||null) return self.parent.limit;
			return null;
		}catch(e){log("WM.rulesManager.Rule.parentLimit: "+e);}});

		this.__defineGetter__("isBranchDisabled",function(){try{
			var p=self.parent,ret=false;
			while(p) {
				if (!p.enabled) return true;
				p=p.parent;
			}
			return false;
		}catch(e){log("WM.rulesManager.Rule.isBranchDisabled: "+e);}});

		this.__defineGetter__("parentLimitCount",function(){try{
			if (self.parent||null) return self.parent.limitCount;
			return null;
		}catch(e){log("WM.rulesManager.Rule.parentLimitCount: "+e);}});

		//copy passed params to this object
		for (var p in params) {
			//omit specific params
			if (!(["actions","validators","kids","eggs"].inArray(p)) ) {
				//copy only params that make it past the checker
				this[p]=params[p];
			}
		}
		
		this.usesRuleButton=function(){
			for (var action in this.actions) {
				if (action.event=="onRuleButtonClicked") {return true;}
			}
			return false;
		};
		
		this.moveUp=function(){try{
			//where is this
			var parentContainer = 
				(this.isChild)?this.parent.kids:
				(this.isEgg)?this.parent.eggs:
				WM.rulesManager.rules;
			//only affects items not already the first in the list
			//and not the only child in the list
			if ((parentContainer.length>1) && (parentContainer[0]!=this)) {
				//which index is this?
				var myIndex=parentContainer.inArrayWhere(this);
				if (myIndex != -1) {
					//I have a proper index here
					//who is my sibling
					var sibling = parentContainer[myIndex-1];
					//swap me with my sibling
					parentContainer[myIndex-1]=this;
					parentContainer[myIndex]=sibling;
					//place my node before my sibling node
					sibling.node.parentNode.insertBefore(this.node,sibling.node);
					//save it
					WM.rulesManager.saveRules();
				}
			}
		}catch(e){log("WM.rulesManager.Rule.moveUp: "+e);}};
		
		this.moveDown=function(){try{
			//where is this
			var parentContainer = 
				(this.isChild)?this.parent.kids:
				(this.isEgg)?this.parent.eggs:
				WM.rulesManager.rules;
			//only affects items not already the last in the list
			//and not the only child in the list
			if ((parentContainer.length>1) && (parentContainer.last()!=this)) {
				//which index is this?
				var myIndex=parentContainer.inArrayWhere(this);
				if (myIndex != -1) {
					//I have a proper index here
					//who is my sibling
					var sibling = parentContainer[myIndex+1];
					//swap me with my sibling
					parentContainer[myIndex+1]=this;
					parentContainer[myIndex]=sibling;
					//place my node before my sibling node
					sibling.node.parentNode.insertBefore(sibling.node,this.node);
					//save it
					WM.rulesManager.saveRules();
				}
			}
		}catch(e){log("WM.rulesManager.Rule.moveDown: "+e);}};

		this.moveUpLevel=function(){try{
			if (this.parent) {
				//this is not a top level node, so we can move it
				var targetContainer=((this.parent.parent)?this.parent.parent.kids:WM.rulesManager.rules);
				//remove from parent
				this.parent[(this.isChild)?"kids":(this.isEgg)?"eggs":null].removeByValue(this);
				//set new parent
				this.parent=(this.parent.parent||null); //never point to the top level
				//set flags
				this.isChild=(this.parent!=null);
				this.isEgg=false;
				//move the object
				targetContainer.push(this);
				//move the node
				if (this.parent) this.parent.kidsNode.appendChild(this.node);
				else WM.console.priorityBuild.appendChild(this.node);
				
				//save it
				WM.rulesManager.saveRules();
			}
		}catch(e){log("WM.rulesManager.Rule.moveUpLevel: "+e);}};
		
		this.moveDownLevel=function(){try{
			//where is this
			var parentContainer = 
				(this.isChild)?this.parent.kids:
				(this.isEgg)?this.parent.eggs:
				WM.rulesManager.rules;
			//create a new rule at my level
			var newRule = new WM.rulesManager.Rule({
				parent:this.parent||null,
				isChild:this.isChild,
				isEgg:this.isEgg,
			});
			parentContainer.push(newRule);
			//remove me from my current parent
			parentContainer.removeByValue(this);
			//attach me to my new parent
			this.parent=newRule;
			this.isChild=true;
			this.isEgg=false;
			newRule.kids.push(this);
			//move my node
			newRule.kidsNode.appendChild(this.node);
			//save it
			WM.rulesManager.saveRules();			
		}catch(e){log("WM.rulesManager.Rule.moveDownLevel: "+e);}};

		this.enable=function(){try{
			this.enabled=true;
			this.node.className=this.node.className.removeWord("disabled");
			WM.rulesManager.saveRules();
		}catch(e){log("WM.rulesManager.Rule.enable: "+e);}};

		this.disable=function(){try{
			this.enabled=false;
			this.node.className=this.node.className.addWord("disabled");
			WM.rulesManager.saveRules();
		}catch(e){log("WM.rulesManager.Rule.disable: "+e);}};

		this.disableChildren=function(){try{
			if (isArrayAndNotEmpty(this.kids)) for (var k=0,kid;(kid=this.kids[k]);k++){
				kid.disable();
			}			
		}catch(e){log("WM.rulesManager.Rule.disableChildren: "+e);}};

		this.enableChildren=function(){try{
			if (isArrayAndNotEmpty(this.kids)) for (var k=0,kid;(kid=this.kids[k]);k++){
				kid.enable();
			}			
		}catch(e){log("WM.rulesManager.Rule.enableChildren: "+e);}};
		
		this.toggle=function(){try{
			//if(this.enabled)this.disable(); else this.enable();
			
			//this.enabled=!this.enabled;
			this.enabled=this.toggleNode.checked;
			this.node.className=this.node.className.swapWordB(this.enabled,"enabled","disabled");
			WM.rulesManager.saveRules();
			//this.toggleNode.checked=();

		}catch(e){log("WM.rulesManager.Rule.toggle: "+e);}};

		this.clone=function(){try{
			var cloneRule=this.saveableData;
			//cloneRule.id=unique();
			if (this.isChild) this.parent.addChild(cloneRule);
			else if (this.isEgg) this.parent.addEgg(cloneRule);
			else WM.rulesManager.newRule(cloneRule);
		}catch(e){log("WM.rulesManager.RuleAction.clone: "+e);}};

		this.resetLimit=function(params){try{
			params=params||{};
			var ask=WM.opts.rulesConfirmResetLimit;
			if (params.noConfirm || !ask || (ask && confirm("Reset Limit Counter?"))) {
				this.limitCount=0;
				this.limitCounterNode.value=this.limitCount;
				if (!(params.resetChildren||false)) {
					if (isArrayAndNotEmpty(this.kids)) for (var k=0,kid;(kid=this.kids[k]);k++){
						kid.resetLimit(params);
					}
				}
				if (!(params.preventSave||false)) WM.rulesManager.saveRules();
			}
		}catch(e){log("WM.rulesManager.Rule.resetLimit: "+e);}};
		
		this.resetBranchLimits=function(params){try{
			params=params||{};
			//resets the limits of entire branch rules, but not self limit
			if (isArrayAndNotEmpty(this.kids)) for (var k=0,kid;(kid=this.kids[k]);k++){
				kid.resetLimit({resetChildren:true,noConfirm:params.noConfirm||false});
			}
		}catch(e){log("WM.rulesManager.Rule.resetBranchLimits: "+e);}};

		this.resetChildrenLimits=function(params){try{
			params=params||{};
			//resets the limits of all immediate children, but not self limit
			if (isArrayAndNotEmpty(this.kids)) for (var k=0,kid;(kid=this.kids[k]);k++){
				kid.resetLimit({noConfirm:params.noConfirm||false});
			}
		}catch(e){log("WM.rulesManager.Rule.resetChildrenLimits: "+e);}};

		this.incrementLimitCounter=function(o,n){try{
			this.limitCount=parseInt(parseInt(this.limitCount)+(exists(n)?parseInt(n):1));
			this.limitCounterNode.value=this.limitCount;
			WM.rulesManager.saveRules();
			//for reaching of limit
			if (this.limit && (this.limitCount>=this.limit)) this.onEvent("onLimit",o);
		}catch(e){log("WM.rulesManager.Rule.incrementLimitCounter: "+e);}};

		this.decrementLimitCounter=function(o,n){try{
			this.limitCount=parseInt(parseInt(this.limitCount)-(exists(n)?parseInt(n):1));
			//dont allow to drop below 0
			if (this.limitCount<0) this.limitCount=0;
			this.limitCounterNode.value=this.limitCount;
			WM.rulesManager.saveRules();
		}catch(e){log("WM.rulesManager.Rule.decrementLimitCounter: "+e);}};

		this.remove=function(noConfirm){try{
			var ask=WM.opts.rulesConfirmDeleteRule;
			if (noConfirm || (this.isGlobal && confirm("This rule is shared with other profiles. Deleting it here will prevent it from loading for other users. Are you sure you wish to delete this rule and its children.")) || !ask || (!this.isGlobal && ask && confirm("Delete rule and all of its child nodes?"))){
				//destroy intervals and timers
				this.cleanup();				
				//remove my data
				var parentContainer=((this.isChild)?this.parent.kids:(this.isEgg)?this.parent.eggs:WM.rulesManager.rules);
				parentContainer.removeByValue(this);
				//remove my node
				remove(this.node);
				
				doAction(WM.rulesManager.saveRules);
			}
		}catch(e){log("WM.rulesManager.Rule.remove: "+e);}};

		this.cancelAllTimers=function(){try{
			//find the correct timer by target
			for (var t in this.timers){
				if (this.timers[t]!=null) {
					window.clearTimeout(this.timers[t]);
					delete this.timers[t];
				}
			}
		}catch(e){log("WM.rulesManager.Rule.cancelAllTimers: "+e);}};		

		this.cancelTimer=function(target){try{
			//find the correct timer by target
			var timer=null;
			for (var t in this.timers){
				if (this.timers[t].target==target) {
					timer=this.timers[t];
					break;
				}
			}
			if (timer) {
				window.clearTimeout(timer.timer);
				delete this.timers[timer.id];
			}
		}catch(e){log("WM.rulesManager.Rule.cancelTimer: "+e);}};
		
		this.createTimer=function(t,o){try{
			this.cancelTimer(o);
			var self=this;
			var stamp=unique();
			var timer=window.setTimeout(function(){
				self.onEvent("onTimer",o);
			},t);
			this.timers[stamp]={timer:timer, target:o, id:stamp};
		}catch(e){log("WM.rulesManager.Rule.createTimer: "+e);}};
		
		this.cancelAllIntervals=function(){try{
			//find the correct timer by target
			for (var t in this.intervals){
				if (this.intervals[t]!=null) {
					window.clearInterval(this.intervals[t]);
					delete this.intervals[t];
				}
			}
		}catch(e){log("WM.rulesManager.Rule.cancelAllIntervals: "+e);}};		
		
		this.cancelInterval=function(target){try{
			//find the correct timer by target
			var interval=null;
			for (var t in this.intervals){
				if (this.intervals[t].target==target) {
					interval=this.intervals[t];
					break;
				}
			}
			if (interval) {
				window.clearInterval(interval.timer);
				delete this.intervals[interval.id];
			}
		}catch(e){log("WM.rulesManager.Rule.cancelInterval: "+e);}};
		
		this.createInterval=function(t,o){try{
			this.cancelInterval(o);
			var self=this;
			var stamp=unique();
			var interval=window.setInterval(function(){
				self.onEvent("onInterval",o);
			},t);
			this.intervals[stamp]={timer:interval, target:o, id:stamp};
		}catch(e){log("WM.rulesManager.Rule.createInterval: "+e);}};
		
		this.doEvent=function(event,obj,logit){try{
			//check to see if post matches this rule, if it does, perform actions
			//if (this.validators){
				//logit=logit||(obj.objType=="post");
				
				var obj=(obj||{});
				//var self=this;
				var matchPost=true, found=[];
				for (var vl=0,validator;(validator=this.validators[vl]);vl++) { try{
					//within the search array, each result is handled as an OR
					var result=false;
					for (var s=0,search; (search=validator.search[s]); s++) {
						var v = 
							//special request for object type of the object that activated this rule
							(search=="activatorType")?(
								(exists(obj))?(obj.objType||"unknown"):"unknown"
							):
							
							//special specific app being paused test
							(search=="isAppPaused")?(
								(exists(obj) && exists(obj.app))?obj.app.paused:false
							):
							
							//special specific bonus type being paused
							(search=="isTypePaused")?(
								(exists(obj) && exists(obj.which) && exists(obj.app))?obj.app.typesPaused.inArray(obj.which):false
							):
							
							//read from post object test data
							(exists(obj) && exists(obj.testData) && exists(obj.testData[search]))?obj.testData[search]:  
							//read from activating object standard parameters
							(exists(obj) && exists(obj[search]))?obj[search]:
							//read from this rule object
							exists(self[search])?self[search]:
							//read from parameters in the console/main object
							exists(WM[search])?WM[search]:
							//there is no known reference for this query
							"undefined";

						//fail validators that do not work with this obj
						if (v=="undefined") {result=false; break;}
						//convert functions to values
						if (typeof v=="function") v=v();
						
						var query = validator.find;
						//make the query the same type as the value
						if (!(typeof query == typeof v)) {
							switch (typeof v) {
								case "boolean":
									//convert texts of false/true to actual booleans
									query = cBool(query);
									break;
								case "number":
									//convert text numbers to real numbers
									query=(query=Number(query))?query:0;
									//if (query==null) query=0;
									break;
							}
						}
						
						//if (logit) log([search, v, query]);	

						//compare
						switch(validator.operand){
							case "equals": result=result||(v==query); break; 
							case "notEquals": result=result||(v!=query); break; 
							case "startsWith": result=result||(v.startsWith(query)); break; 
							case "notStartsWith": result=result||(!v.startsWith(query)); break; 
							case "endsWith": result=result||(v.endsWith(query)); break; 
							case "notEndsWith": result=result||(!v.endsWith(query)); break; 
							case "contains": result=result||(v.contains(query)); break; 
							case "notContains": result=result||(!v.contains(query)); break; 
							case "greaterThan": result=result||(v>query); break; 
							case "lessThan": result=result||(v<query); break; 
							case "greaterThanOrEquals": result=result||(v>=query); break; 
							case "lessThanOrEquals": result=result||(v<=query); break; 
							case "matchRegExp":
								var f; //storage space for match array
								var regex = new RegExp(query,"gi");
								f=regex.exec(v);
								result=result||isArrayAndNotEmpty(f);
								//result=result||((f=v.match(regex))!=null);
								if (f) found=found.concat(f);
								break;
							case "notMatchRegExp":
								var regex = new RegExp(query,"gi");
								result=result||(v.match(regex)==null);
								break;
							case "equalsExactly": result=result||(v===query); break; 
							case "notEqualsExactly": result=result||!(v===query); break; 
						}
						//any result of true inside this loop is an automatic success
						if (result) break;
					}
					//evaluate our current result with the previous results
					//outside the search array, each value is handled as an AND
					//any one non-match is a complete failure
					matchPost=(matchPost && result);
					if (!matchPost) break;
				}catch(e){
					log("WM.rulesManager.Rule.doEvent: "+e+"{event:" +event+ ", search:"+search+", value:"+v+", query:"+query+", result:"+result+"}");
					continue;
				}}
				//if after all testing we still matched the object
				//then perform this rule's events and check children
				if (matchPost) {
					//log("post matches all validators");
					//first do every child rule
					for (var k=0,kid;(kid=this.kids[k]);k++){
						kid.doEvent(event,obj,true);
					}
					//now finish up with this rule's actions
					this.onEvent(event,obj,found||null);
				}
			//}
			
			//log("WM.rulesManager.Rule.doEvent: {obj="+obj.id+", event="+event+", matchPost="+matchPost+"}");
		}catch(e){log("WM.rulesManager.Rule.doEvent: "+e);}};

		this.onEvent=function(event,obj,found){try{
			var actionFilter=["*"];
			/*
				handle special events
			*/
			if (event=="onRuleCreated") {
				/*
					we do want onRuleCreated events to fire even if the rule is disabled,
					or intervals won't be set and ready for later, if the user does enable the rule
					this session. But we want to filter which actions are available.
				*/
				if (!this.enabled || this.isBranchDisabled) actionFilter=["createInterval","createTimer"];
				
			} else if ((event=="onInterval")||(event=="onTimer")) {
				//special case for intervals and timers
				if (!this.enabled || this.isBranchDisabled) return;
			} else {
				//always break if this rule is disabled
				if (!this.enabled || this.isBranchDisabled) return;
			}
			/*
				end handle special events
			*/
		
			obj=obj||null;
			//var self=this;
			var post=(self!=obj)?obj:null;
			var app=post?(exists(obj.app)?obj.app:obj):null;
			
			//some insertable post parts
			var inserts=["appID","which","fromID"];
			
			//perform an action based on an event call
			//post object may be null if called from this
			for (var a1=0,action;(action=this.actions[a1]);a1++){
				//filter actions: allow only those in the filter list, or all actions if "*" is in the list				
				if (actionFilter.inArray("*") || actionFilter.inArray(action.action) ) if (action.event==event){
					var param=action.params;
					var param2=action.params2;
					var hasParam=action.hasParam;
					//format some post parts into the param
					if (hasParam && param) {
						for (var i=0;i<inserts.length;i++){
							if (post && (post.replace||null)) {
								param.replace(new RegExp("{%"+inserts[i]+"}","gi"), post.testData[inserts[i]] || post[inserts[i]]);
							}
						}
					}
					
					switch(action.action){
						case "destroyRule":(function(){
							doAction(function(){
								self.remove(true);
							});
							})(); break;
						case "pauseType":(function(){
							var w=post.which, a=app;
							doAction(function(){
								WM.pauseByType(a,w);
							});
							})(); break;
						case "unpauseType":(function(){
							var w=post.which, a=app;
							doAction(function(){
								WM.unPauseByType(a,w);
							});
							})(); break;
						case "uncheckType":(function(){
							var w=post.which, a=app;
							doAction(function(){
								WM.disableOpt(w,a);
								//WM.stopCollectionOf(post.which);
							});
							})(); break;

						case "checkType":(function(){
							var w=post.which, a=app;
							doAction(function(){
								WM.enableOpt(w,a);
								//WM.startCollectionOf(post.which);
							});
							})(); break;
						case "disableAppOption":(function(){
							var c=param, f=found, a=app;
							if (f) c=c.format2(f);
							doAction(function(){
								WM.disableOpt(c,a);
							});
							})(); break;
						case "enableAppOption":(function(){
							var c=param, f=found, a=app;
							if (f) c=c.format2(f);
							doAction(function(){
								WM.enableOpt(c,a);
							});
							})(); break;
						case "disableHostOption":(function(){
							//debug.print("option disabled");
							var c=param, f=found;
							if (f) c=c.format2(f);
							doAction(function(){
								WM.disableOpt(c);
							});
							})(); break;
						case "enableHostOption":(function(){
							//debug.print("option enabled");
							var c=param, f=found;
							if (f) c=c.format2(f);
							doAction(function(){
								WM.enableOpt(c);
							});
							})(); break;
						case "disableAutolike":(function(){
							doAction(function(){
								//debug.print("autolike disabled");
								WM.disableOpt("useautolike");
							});
							})(); break;
						case "enableAutolike":(function(){
							doAction(function(){
								//debug.print("autolike enabled");
								WM.enableOpt("useautolike");
							});
							})(); break;
						case "disableAutocomment":(function(){
							doAction(function(){
								WM.disableOpt("useautocomment");
							});
							})(); break;
						case "enableAutocomment":(function(){
							doAction(function(){
								WM.enableOpt("useautocomment");
							});
							})(); break;
						case "pauseApp":(function(){
							var a = WM.apps[param]||app;
							doAction(function(){
								a.pause();
							});
							})(); break;
						case "unpauseApp":(function(){
							var a = WM.apps[param]||app;
							doAction(function(){
								a.unPause();
							});
							})(); break;
						case "appendLink":(function(){
							var p=post, v=param||"";
							if (p) doAction(function(){
								p.link=p.linkHref+v;
							});
							})(); break;
						case "unpauseAllTypesByApp":(function(){
							var a = WM.apps[param]||app;
							doAction(function(){
								a.unpauseAllTypes();
							});
							})(); break;
						case "unpauseAllTypesAllApps":(function(){
							doAction(function(){
								for (var a in WM.apps){
									a.unpauseAllTypes();
								}
							});
							})(); break;
						case "unpauseAllApps":(function(){
							doAction(function(){
								for (var a in WM.apps){
									a.unpause();
								}
							});
							})(); break;
						case "pauseAllApps":(function(){
							doAction(function(){
								for (var a in WM.apps){
									a.pause();
								}
							});
							})(); break;
						case "refreshBrowser":(function(){
							doAction(function(){
								window.location.reload();
							});
							})(); break;
						case "pauseWM.collector":(function(){
							doAction(function(){
								WM.pauseCollecting(true);
							});
							})(); break;
						case "unpauseWM.collector":(function(){
							doAction(function(){
								WM.pauseCollecting(false);
							});
							})(); break;
						case "pauseFetch":(function(){
							doAction(function(){
								WM.pauseFetching(true);
							});
							})(); break;
						case "unpauseFetch":(function(){
							doAction(function(){
								WM.pauseFetching(false);
							});
							})(); break;
						case "likePost":(function(){
							doAction(function(){
								post.like();
							});
							})(); break;
						case "queueLikePost":(function(){
							doAction(function(){
								WM.queAutoLike(post);
							});
							})(); break;
						case "commentPost":(function(){
							var p=param,f=found;
							if (f) p=p.format2(f);
							doAction(function(){
								post.comment(p);
							});
							})(); break;						
						case "queueCommentPost":(function(){
							var p=param,f=found;
							if (f) p=p.format2(f);
							//log(["queueCommentPost fired",p]);
							doAction(function(){
								WM.queAutoComment(post,p);
							});
							})(); break;
						case "cleanPost":(function(){
							doAction(function(){
								post.remove();
							});
							})(); break;
						case "incrementCounter":(function(){
							var o=obj,p=param,f=found;
							//if (f) p=p.format2(f);
							doAction(function(){
								self.incrementLimitCounter(o,p);
							});
							})(); break;
						case "decrementCounter":(function(){
							var o=obj,p=param,f=found;
							//if (f) p=p.format2(f);
							doAction(function(){
								self.decrementLimitCounter(o,p);
							});
							})(); break;
						case "incrementParentCounter":(function(){
							var o=obj,p=param, f=found;
							//if (f) p=p.format2(f);
							if (this.parent) {
								doAction(function(){
									//passes the activating object, not this rule
									self.parent.incrementLimitCounter(o,p);
								});
							}
							})(); break;
						case "decrementParentCounter":(function(){
							var o=obj,p=param, f=found;
							//if (f) p=p.format2(f);
							if (this.parent){
								doAction(function(){
									//passes the activating object, not this rule
									self.parent.decrementLimitCounter(o,p);
								});
							}
							})(); break;
						case "setColor":(function(){
							var c=param;
							var f=found;
							if (f) c=c.format2(f);
							doAction(function(){
								post.setColor(c);
							});
							})(); break;
						case "pinPost":(function(){
							doAction(function(){
								post.pin();
							});
							})(); break;
						case "setAsAccepted":(function(){
							var saveit=param;
							doAction(function(){
								post.accept(saveit);
							});
							})(); break;
						case "setAsFailed":(function(){
							var saveit=param;
							doAction(function(){
								post.fail(saveit);
							});
							})(); break;
						case "setAsExcluded":(function(){
							doAction(function(){
								post.exclude();
							});
							})(); break;
						case "processFirst":(function(){
							doAction(function(){
								post.moveToTop();
							});
							})(); break;
						case "processLast":(function(){
							doAction(function(){
								post.moveToBottom();
							});
							})(); break;
						case "setPriority":(function(){
							var p=param, f=found;
							if (f) p=p.format2(f);
							doAction(function(){
								post.setPriority(p);
							});
							})(); break;
						case "setPriorityApp":(function(){
							var p=param2, a=WM.apps[param]||app, f=found;
							if (f) p=p.format2(f);
							doAction(function(){
								app.setPriority(p);
							});
							})(); break;
						case "removePriorityApp":(function(){
							var p=param2, a=WM.apps[param]||app, f=found;
							if (f) p=p.format2(f);
							doAction(function(){
								app.setPriority(50);
							});
							})(); break;
						case "setPriorityType":(function(){
							var p=param2, a=app, f=found, w=param||post.which;
							if (f) p=p.format2(f);
							doAction(function(){
								app.setPriorityByType(w,p);
							});
							})(); break;
						case "removePriorityType":(function(){
							var a=app, f=found, w=param||post.which;
							if (f) p=p.format2(f);
							doAction(function(){
								app.setPriorityByType(w,50);
							});
							})(); break;
						case "removePriority":(function(){
							doAction(function(){
								post.setPriority(50);
							});
							})(); break;
						case "resetLimit":(function(){
							doAction(function(){
								self.resetLimit({noConfirm:true});
							});
							})(); break;
						case "resetParentLimit":(function(){
							if (this.parent) {
								doAction(function(){
									self.parent.resetLimit({noConfirm:true});
								});
							}
							})(); break;
						case "resetChildrenLimits":(function(){
							doAction(function(){
								self.resetChildrenLimits({noConfirm:true});
							});
							})(); break;						
						case "resetBranchLimits":(function(){
							doAction(function(){
								self.resetBranchLimits({noConfirm:true});
							});
							})(); break;				
						case "hatch":(function(){
							var o=obj;
							doAction(function(){
								self.hatch(o);
							});
							})(); break;
						case "birth":(function(){
							var o=obj;
							doAction(function(){
								this.birth(o);
							});
							})(); break;
						case "fetchNewer":(function(){
							doAction(function(){
								app.fetchPosts({newer:true,bypassPause:true});
							});
							})(); break;
						case "fetchOlder":(function(){
							doAction(function(){
								app.fetchPosts({older:true,bypassPause:true});
							});
							})(); break;
						case "fetchHours":(function(){
							var p=param, f=found, a=app;
							if (f) p=p.format2(f);
							doAction(function(){
								//var t0=timestamp()/1000; //let the fetch script calc it from the feed
								var t1=Math.round((timeStamp()-(p*hour))/1000);
								//t=t.substr(0,t.length-3);
								log("fetchHours: "+p+" please wait...");
								WM.fetch({bypassPause:true, older:true, targetEdge:t1, currentEdge:Math.round(timeStamp()/1000), apps:app});
							});
							})(); break;
						case "disableRule":(function(){
							doAction(function(){						
								self.disable();
							});
							})(); break;
						case "enableRule":(function(){
							doAction(function(){
								self.enable();
							});
							})(); break;
						case "disableChildRules":(function(){
							doAction(function(){						
								self.disableChildren();
							});
							})(); break;
						case "enableChildRules":(function(){
							doAction(function(){
								self.enableChildren();
							});
							})(); break;
						case "disableApp":(function(){
							//check for specified app
							var a = WM.apps[param]||app;
							doAction(function(){
								a.disable();
							});
							})(); break;
						case "enableApp":(function(){
							var a = WM.apps[param]||app;
							doAction(function(){
								a.enable();
							});
							})(); break;
						case "forceOpen":(function(){
							doAction(function(){
								post.forceOpen();
							});
							})(); break;
						case "forceOpenFirst":(function(){
							doAction(function(){
								post.forceOpen({first:true});
							});
							})(); break;
						case "emergencyOpen":(function(){
							doAction(function(){
								post.forceOpen({emergency:true});
							});
							})(); break;
						case "setToCollect":(function(){
							doAction(function(){
								post.collect();
							});
							})(); break;
						case "setToCollectPriority1":(function(){
							doAction(function(){
								post.collect();
								post.setPriority(1);
							});
							})(); break;
						case "createTimer":(function(){
							var o=obj, p=param, f=found;
							if (f) p=p.format2(f);
							//allow new time format entry
							//if the calculated time differs from the passed time, then use that calculated time, as long as it doesn't translate to 0
							var t=calcTime(p);
							if (t!=0 && t!=p) p=t;
							//debug.print(["b",param,t,p]);
							doAction(function(){
								self.createTimer(p,o);
							});
							})(); break;
						case "cancelTimer":(function(){
							var o=obj;
							doAction(function(){
								if (o.objType=="rule") self.cancelAllTimers();
								else self.cancelTimer(o);
							});
							})(); break;
						case "createInterval":(function(){
							var o=obj, p=param, f=found;
							if (f) p=p.format2(f);
							//allow new time format entry
							//if the calculated time differs from the passed time, then use that calculated time, as long as it doesn't translate to 0
							var t=calcTime(p);
							if (t!=0 && t!=p) p=t;
							//debug.print(["b",param,t,p]);
							doAction(function(){
								self.createInterval(p,o);
							});
							})(); break;
						case "cancelInterval":(function(){
							var o=obj;
							doAction(function(){
								if (o.objType=="rule") self.cancelAllIntervals();
								else self.cancelInterval(o);
							});
							})(); break;
						case "setWhich":(function(){
							var w=param;
							var f=found;
							if (f) w=w.format2(f);
							doAction(function(){
								post.setWhich(w);
							});
							})(); break;
						case "reIDAll": (function(){
							doAction(function(){
								WM.reIDAll();
							});
							})(); break;
						case "resetAllLimits":(function(){
							doAction(function(){
								WM.rulesManager.resetAllLimits();
							});
							})(); break;							
						case "openPostSource":(function(){
							doAction(function(){
								post.openSource();
							});
							})(); break;
						case "emptyAutolikeQueue":(function(){
							doAction(function(){
								WM.emptyAutoLikeQueue();
							});
							})(); break;							
						case "setHostOption":(function(){
							var c=param, c2=param2, f=found;
							if (f) c=c.format2(f); //format only param1
							doAction(function(){
								WM.setOpt(c,c2);
							});
							})(); break;
						case "setAppOption":(function(){
							var c=param, c2=param2, f=found, a=app;
							if (f) c=c.format2(f); //format only param1
							doAction(function(){
								WM.setOpt(c,c2,a);
							});
							})(); break;							
						case "setAppTab":(function(){
							if (param=="all") {
								doAction(function(){
									//switch to Show All
									WM.console.collectTabControl.selectTab(0);
								});
							} else {
								//check for specified app
								var a = WM.apps[param]||app;
								if (a||null) doAction(function(){
									//switch to associated tab
									click(a.collectionTabNode);
								});
							}})(); break;							
					}	
				}
			}
		}catch(e){log("WM.rulesManager.Rule.onEvent: "+e);}};
		
		this.addAction=function(p){try{
			var isNew=!exists(p);
			p=p||{};
			p.parent=this;
			var ret=new WM.rulesManager.RuleAction(p);
			this.actions.push(ret);
			if (isNew) WM.rulesManager.saveRules();
		}catch(e){log("WM.rulesManager.Rule.addAction: "+e);}};

		this.addValidator=function(p){try{
			var isNew=!exists(p);
			p=p||{};
			p.parent=this;
			var ret=new WM.rulesManager.RuleValidator(p);
			this.validators.push(ret);
			if (isNew) WM.rulesManager.saveRules();
		}catch(e){log("WM.rulesManager.Rule.addValidator: "+e);}};
		
		this.addChild=function(p){try{
			var isNew=!exists(p);
			p=p||{};
			p.parent=this;
			p.isChild=true;
			var rule=new WM.rulesManager.Rule(p);
			this.kids.push(rule);
			if (isNew) WM.rulesManager.saveRules();
		}catch(e){log("WM.rulesManager.Rule.addChild: "+e);}};

		this.addEgg=function(p){try{
			var isNew=!exists(p);
			p=p||{};
			p.parent=this;
			p.isEgg=true;
			var rule=new WM.rulesManager.Rule(p);
			this.eggs.push(rule);
			if (isNew) WM.rulesManager.saveRules();
		}catch(e){log("WM.rulesManager.Rule.addEgg: "+e);}};		
		
		//move eggs to parent node and destroy this node
		this.hatch=function(obj){try{
			var ask=WM.opts.rulesConfirmHatch
			if (!ask || (ask && confirm("Hatch egg child and remove current rule and all its children?")) ) {
				this.onEvent("onHatch",obj||this);
				for (var e=0,egg; (egg=this.eggs[e]); e++){
					egg.moveUpLevel();
				}
				this.remove(true); //with noConfirm
			}
		}catch(e){log("WM.rulesManager.Rule.hatch: "+e);}};

		//clone eggs to parent node
		this.birth=function(obj){try{
			this.onEvent("onBirth",obj||this);
			for (var e=0,egg; (egg=this.eggs[e]); e++){
				var cloneRule=egg.saveableData;
				if (this.isChild) this.parent.addChild(cloneRule);
				else WM.rulesManager.newRule(cloneRule);
			}
		}catch(e){log("WM.rulesManager.Rule.birth: "+e);}};

		//self rule button clicked
		this.ruleButtonClicked=function(obj){try{
			this.onEvent("onRuleButtonClicked",obj||this);
		}catch(e){log("WM.rulesManager.Rule.ruleButtonClicked: "+e);}};

		this.toggleContent=function(){try{
			this.expanded=!this.expanded;
			var btnSize=WM.opts.littleButtonSize;
			with (this.contentNode)
				className=className.swapWordB(this.expanded,"expanded","collapsed");
			with (this.toggleImgNode)
				className=className.swapWordB(this.expanded,"treeCollapse"+btnSize,"treeExpand"+btnSize);
			WM.rulesManager.saveRules();
		}catch(e){log("WM.rulesManager.Rule.toggleContent: "+e);}};

		this.populateBonusList=function(){try{
			var node=this.bonusDropDown;
			var bonuses=[];
			//get the list of accept texts for this app
			if (this.appID!="") {
				if (this.appID=="* All") {
					//populate list with bonuses from ALL docked sidekicks
				} else bonuses = mergeJSON(WM.apps[this.appID].accText,WM.apps[this.appID].userDefinedTypes);
			}
			bonuses["dynamic"]="* Dynamic grab";
			bonuses["none"]="* None";
			bonuses["wishlist"]="* Flaged as Wishlist";
			bonuses["exclude"]="* Excluded types";
			bonuses["send"]="* Send Unknown";
			bonuses["doUnknown"]="* Get Unknown";
			bonuses["*"]="* All"; //perform rule on ALL bonus types for this app

			//sort by display text
			bonuses=sortCollection(bonuses,"value");

			//add each element to the dropdown
			var elem;
			node.innerHTML=""; //wipe previous list
			for (var i in bonuses) {
				var showI=i.removePrefix(this.appID);
				node.appendChild(elem=createElement("option",{textContent:((bonuses[i].startsWith("*"))?"":((showI.startsWith("send"))?"Send ":"Get "))+bonuses[i], value:showI}));
				if (this.bonus== showI) elem.selected = true;
			}
		}catch(e){log("WM.rulesManager.Rule.populateBonusList: "+e);}};

		//draw to priority/rule manager or to the parent node's kids or eggs section
		try{(((this.parent)?this.parent[(this.isChild)?"kidsNode":"eggsNode"]:null)||$("wmPriorityBuilder")).appendChild(
			this.node=createElement("div",{className:"listItem "+((this.enabled)?"enabled":"disabled")},[
				createElement("div",{className:"line"},[
					createElement("div",{className:"littleButton",title:"Toggle Content",onclick:function(){self.toggleContent();}},[
						this.toggleImgNode=createElement("img",{className:"resourceIcon "+(this.expanded?"treeCollapse"+WM.opts.littleButtonSize:"treeExpand"+WM.opts.littleButtonSize)}),
					]),
					this.toggleNode=createElement("input",{type:"checkbox",checked:this.enabled,onchange:function(){
						self.enabled=this.checked;
						with (self.node) className=className.toggleWordB(!this.checked,"disabled");
						WM.rulesManager.saveRules();
					}}),
					createElement("label",{textContent:"Title:"}),
					this.titleNode=createElement("input",{className:"w400",value:(this.title||""), onchange:function(){self.title=this.value; WM.rulesManager.saveRules();}}),
					
					//toolbox
					createElement("div",{className:"littleButton oddOrange", title:"Remove Rule"},[
						createElement("img",{className:"resourceIcon trash"+WM.opts.littleButtonSize,onclick:function(){self.remove();}})]),
					createElement("div",{className:"littleButton oddBlue",title:"Hatch Egg Children"},[
						createElement("img",{className:"resourceIcon hatch"+WM.opts.littleButtonSize,onclick:function(){self.hatch();}})]),
					createElement("div",{className:"littleButton oddBlue", title:"Reset Limit Counter"},[
						createElement("img",{className:"resourceIcon reset"+WM.opts.littleButtonSize,onclick:function(){self.resetLimit();}})]),
					createElement("div",{className:"littleButton oddBlue", title:"Clone Rule"},[
						createElement("img",{className:"resourceIcon clone"+WM.opts.littleButtonSize,onclick:function(){self.clone();}})]),
					createElement("div",{className:"littleButton oddBlue", title:"Birth Egg Children"},[
						createElement("img",{className:"resourceIcon birth"+WM.opts.littleButtonSize,onclick:function(){self.birth();}})]),
					createElement("div",{className:"littleButton oddGreen", title:"Move Up"},[
						createElement("img",{className:"resourceIcon arrowUp"+WM.opts.littleButtonSize,onclick:function(){self.moveUp();}})]),
					createElement("div",{className:"littleButton oddOrange", title:"Move Down"},[
						createElement("img",{className:"resourceIcon arrowDown"+WM.opts.littleButtonSize,onclick:function(){self.moveDown();}})]),
					createElement("div",{className:"littleButton oddGreen", title:"Move Up Level"},[
						createElement("img",{className:"resourceIcon moveUpLevelLeft"+WM.opts.littleButtonSize,onclick:function(){self.moveUpLevel();}})]),
					createElement("div",{className:"littleButton oddOrange", title:"Move Down Level"},[
						createElement("img",{className:"resourceIcon moveInLevel"+WM.opts.littleButtonSize,onclick:function(){self.moveDownLevel();}})]),
					createElement("div",{className:"littleButton oddBlue", title:"Show Source"},[
						createElement("img",{className:"resourceIcon object"+WM.opts.littleButtonSize,onclick:function(){promptText(JSON.stringify(self.saveableData),true);}})]),

					createElement("div",{className:"indent littleButton "+((this.isGlobal)?"oddOrange":"oddGreen"), title:((this.isGlobal)?"Disable Profile Sharing":"Share With Other Profiles")},[
						this.toggleGlobalButton=createElement("img",{className:"resourceIcon "+((this.isGlobal)?"removeGlobal":"addGlobal")+WM.opts.littleButtonSize,onclick:function(){self.isGlobal=!self.isGlobal; WM.rulesManager.saveRules();}})]),
				]),
				this.contentNode=createElement("div",{className:"subsection "+(this.expanded?"expanded":"collapsed")},[
					(this.basedOn)?createElement("div",{className:"line"},[
						createElement("label",{textContent:"This rule is linked to a post: ",title:"This rule is linked to a post. Validators can draw information from that post so you can easily capture similar posts just by editing the captured texts to suit your needs. Post linking is not carried from session to session."}),
						this.basedOnNode=createElement("span",{textContent:this.basedOn.id}),
					]):null,
					createElement("div",{className:"line"},[
						createElement("label",{textContent:"Limit:"}),
						this.limitNode=createElement("input",{value:(this.limit||0), onchange:function(){self.limit=this.value;WM.rulesManager.saveRules();}}),
					]),
					createElement("div",{className:"line"},[
						createElement("label",{textContent:"Counter:"}),
						this.limitCounterNode=createElement("input",{value:(this.limitCount||0), onchange:function(){self.limitCount=this.value;WM.rulesManager.saveRules();}}),
					]),
					this.ruleButtonHousingNode=createElement("div",{className:"line", style:(this.usesRuleButton())?"":"display:none;"},[
						createElement("label",{textContent:"Rule Button:"}),
						this.ruleButtonNode=createElement("button",{type:"button", textContent:"onRuleButtonClicked()", onclick:function(){self.ruleButtonClicked();}}),
					]),
					//validation subbox
					createElement("div",{className:"line"},[
						createElement("label",{textContent:"For Activating Objects:",title:"These validators attempt to match a post or other activating object, such as feed, feed filter, app, or this rule. All activators that match here then have the following actions performed at certain events."}),
						createElement("div",{className:"littleButton oddGreen",onclick:function(){self.addValidator();},title:"Add Validator"},[
							createElement("img",{className:"resourceIcon plus"+WM.opts.littleButtonSize}),
						]),
						this.validationNode=createElement("div",{className:"subsection"}),
					]),				
					//actions subbox
					createElement("div",{className:"line"},[
						createElement("label",{textContent:"Do Actions:",title:"Actions to perform on matching posts."}),
						createElement("div",{className:"littleButton oddGreen",onclick:function(){self.addAction();},title:"Add Action"},[
							createElement("img",{className:"resourceIcon plus"+WM.opts.littleButtonSize}),
						]),
						this.actionsNode=createElement("div",{className:"subsection"}),
					]),
					//kids subbox
					createElement("div",{className:"line"},[
						createElement("label",{textContent:"Child Rules:",title:"Child rules are nested rules which are applied to matching posts at the same time the parent rule is applied. Child rules can have different validators, but will only activate if the parent validators have already matched a post."}),
						createElement("div",{className:"littleButton oddGreen",onclick:function(){self.addChild();},title:"Add Child"},[
							createElement("img",{className:"resourceIcon plus"+WM.opts.littleButtonSize}),
						]),
						this.kidsNode=createElement("div",{className:"subsection"}),
					]),
					//egg kids subbox
					createElement("div",{className:"line"},[
						createElement("label",{textContent:"Egg Rules:", title:"Eggs are potential future rules. When 'hatched', these eggs take the place of the parent rule. The parent rule and its normal children are destroyed."}),
						createElement("div",{className:"littleButton oddGreen",onclick:function(){self.addEgg();},title:"Add Egg"},[
							createElement("img",{className:"resourceIcon plus"+WM.opts.littleButtonSize}),
						]),
						this.eggsNode=createElement("div",{className:"subsection"}),
					]),
				]),
			])
		);}catch(e){log("WM.rulesManager.Rule.init.drawRule: "+e);}

		
		//list the actions for this rule
		if (isArrayAndNotEmpty(params.actions)) for (var i=0,action; (action=params.actions[i]); i++) {
			this.addAction(action);
		}
		//list the validators for this rule
		if (isArrayAndNotEmpty(params.validators)) for (var i=0,validator; (validator=params.validators[i]); i++) {
			this.addValidator(validator);
		}
		//list the kids for this rule
		if (isArrayAndNotEmpty(params.kids)) for (var i=0,kid; (kid=params.kids[i]); i++) {
			this.addChild(kid);
		}
		//list the egg kids for this rule
		if (isArrayAndNotEmpty(params.eggs)) for (var i=0,egg; (egg=params.eggs[i]); i++) {
			this.addEgg(egg);
		}
				
		//create cleanup function
		this.cleanup=function(){try{
			for (var t in this.timers) {
				window.clearTimeout(this.timers[t]);
			}
			for (var i in this.intervals) {
				window.clearInterval(this.intervals[i]);
			}
			var self=this;
			removeEventListener("beforeunload",self.cleanup,false);
		}catch(e){log("WM.rulesManager.Rule.cleanup: "+e);}};
		addEventListener("beforeunload",self.cleanup,false);
		
		this.onEvent("onRuleCreated");
		return self;

	}catch(e){log("WM.rulesManager.Rule.init: "+e);}};

//***************************************************************************************************************************************
//***** App Class
//***************************************************************************************************************************************
	WM.App = function(params){try{
		this.objType="app";
		var self=this;
	
		//expected: id, name, namespace, icon
		params=params||{};

		//create the masterswitch
		var testms=WM.quickOpts.masterSwitch[params.appID];
		WM.quickOpts.masterSwitch[params.appID]=(testms==null||testms=="undefined")?true:testms;

		//set defaults
		this._enabled=WM.quickOpts.masterSwitch[params.appID]||false;
		this._paused=false;
		this.tests={};
		this.typesPaused=[];
		this.pausedTypesListNodes={};
		this._acceptCount=0;
		this._failCount=0;
		this.node=null;
		this.expanded=false;
		this.kids=[]; //contains additional filtered apps
		
		//setup config for this sidekick
		this.opts = {};
		this.config = new Config({
			storageName:"settings_"+params.appID+"_"+(WM.quickOpts.useGlobalSettings?"global":WM.currentUser.profile),
			onSave:WM.onSave,
			title:"FB Wall Manager "+WM.version+(WM.quickOpts.useGlobalSettings?" (!! Global Settings !!)":""),
			logo:createElement("span",{}[
				createElement("img",{className:"logo",src:"",textContent:"v"+WM.version}),
				createElement("text","v"+WM.version)
			]),
			css:(
				WM.console.dynamicIcons()+
				jsForms.globalStyle()
			),
			settings:{
				btn_useGlobal:{
					type:"button",
					label:"Use Global Settings", 
					title:"Switch to using a global storage for settings. Those settings can then be used by other accounts (not browser profiles).",
					script:function(){
						if (WM.quickOpts.useGlobalSettings||false) {
							//already using global settings
							return;
						}
						if (confirm("Switch to using global (shared) settings?")){
							WM.quickOpts.useGlobalSettings=true;
							WM.saveQuickOpts();
							this.config.title = "FB Wall Manager "+WM.version+" (!! Global Settings !!))";
							this.config.storageName = "settings_"+params.appID+"_global";
							this.config.values=this.config.read();
							this.config.configure();
							this.config.reload();
						}
					},
				},
				btn_useOwnProfile:{
					type:"button",
					label:"Use Profile Settings", 
					title:"Switch to using your own profile storage for settings.",
					script:function(){
						if (!(WM.quickOpts.useGlobalSettings||false)) {
							//already using profile settings
							return;
						}
						if (confirm("Switch to using your own profile settings?")){
							WM.quickOpts.useGlobalSettings=false;
							WM.saveQuickOpts();
							this.config.title = "FB Wall Manager "+WM.version;
							this.config.storageName = "settings_"+params.appID+"_"+WM.currentUser.profile;
							this.config.values=this.config.read();
							this.config.configure();
							this.config.reload();
						}
					},
				},
			
			},
		});
		
		//setup user defined accept texts
		try{
			if (WM.quickOpts.userDefinedTypes) {
				this.userDefinedTypes=WM.quickOpts.userDefinedTypes[params.appID]||{};
			} else {
				WM.quickOpts.userDefinedTypes={};
				WM.quickOpts.userDefinedTypes[params.appID]={};
				WM.saveQuickOpts();
			}
		}catch(e){log("WM.App.init: userDefinedTypes: "+e);}

		//use passed params
		for (var p in params) this[p]=params[p];

		//enable/disable all sidekick functions
		this.enable=function(){try{this.enabled=true;}catch(e){log("WM.App.enable: "+e);}};
		this.disable=function(){try{this.enabled=false;}catch(e){log("WM.App.disable: "+e);}};
		this.toggle=function(){try{this.enabled=!this.enabled;}catch(e){log("WM.App.toggle: "+e);}};

		//pause collection for this app
		this.pause=function(){try{this.paused=true;}catch(e){log("WM.App.pause: "+e);}}
		this.unPause=function(){try{this.paused=false;}catch(e){log("WM.App.unPause: "+e);}}

		//user defined types
		this.addUDT=function(params,drawOnly){try{
			//validate params or ask for input
			if (!exists(params) || !params.id) {
				params=params||{};
				var udtname=prompt("Enter the text name of the bonus type you wish to make (ie 'Horse')\n","");
				var udtid=this.appID+udtname.noSpaces().toLowerCase();
				udtid=prompt("OK, your type will read as '"+udtname+"'.\nNow modify this bonus type code to suit your needs.\n\nTip: You should prefix this code with the appID '"+this.appID+"', but it is not required.\nTip: Most sidekicks use lowercase and no spaces, but again, this is not a requirement.\n", udtid);
				if (udtid.trim()){
					params.id=udtid.trim();
					params.name=udtname;
				} else {
					alert("You supplied a blank user defined type ID. No type was created.");
					return false;
				}
			}
			if (!drawOnly){
				this.userDefinedTypes[params.id]=params.name;
				WM.quickOpts.userDefinedTypes[this.appID]=this.userDefinedTypes;
				WM.saveQuickOpts();
			}
			
			//draw the udt node
			if (this.udtNode){
				this.udtNode.appendChild(
					createElement("div",{className:"listItem"},[
						createElement("label",{textContent:params.id+" : "}),
						createElement("input",{value:params.name,title:"The display name of this type, used wherever bonus types are identified or selected.", onchange:function(){
							self.userDefinedTypes[params.id]=this.value;
							WM.quickOpts.userDefinedTypes[self.appID]=self.userDefinedTypes;
							WM.saveQuickOpts();
						}}),
						createElement("div",{className:"littleButton oddOrange", title:"Remove User-Defined Type"},[
							createElement("img",{className:"resourceIcon trash" +WM.opts.littleButtonSize,onclick:function(){
								var ask=WM.opts.appsConfirmDeleteUDT;
								if (!ask || (ask && confirm("Delete User Defined Type?"))) {
									delete self.userDefinedTypes[params.id];
									WM.quickOpts.userDefinedTypes[self.appID]=self.userDefinedTypes;
									WM.saveQuickOpts();
									remove (this.parentNode.parentNode);
								}
							}})
						]),
						(this.accText[params.id]||null)?createElement("span",{title:"The type id you created exactly matches one provided by the sidekick for this app. If you did not intend to overwrite that bonus's display text, you may wish to create another type id and destroy this one.",style:"color:red;",textContent:"Overwrites a sidekick-provided bonus type id."}):null,
					])
				);
			}			
		}catch(e){log("WM.App.addUDT: "+e);}}

		//unpause all bonus types for this app
		this.unpauseAllTypes=function(){try{
			for (var i=this.typesPaused.length-1;i>=0;i--){
				WM.unPauseByType(this,this.typesPaused[i]);
			}
		}catch(e){log("WM.App.unpauseAllTypes: "+e);}};


		//mass set priority for entire app post collection
		this.setPriority=function(n){try{
			for (var p in WM.posts) {
				var post=WM.posts[p];
				if (post.app==this) post.setPriority(n);
			}
		}catch(e){log("WM.App.setPriority: "+e);}};

		//mass set priority for all posts of type
		this.setPriorityByType=function(w,n){try{
			for (var p in WM.posts) {
				var post=WM.posts[p];
				if (post.app==this && post.which==w) post.setPriority(n);
			}
		}catch(e){log("WM.App.setPriorityByType: "+e);}};
		
		//reset accept/fail counters
		this.resetCounter=function(){try{
			this.acceptCount=0; 
			this.failCount=0;
		}catch(e){log("WM.App.resetCounter: "+e);}};

		//reset all config options for this app
		//except those outside the standard branch (dontsteal,blockautolike,etc.)
		this.resetConfig=function(){try{
			var ask=WM.opts.configConfirmRestore;
			if (!ask || (ask && confirm("Restore sidekick settings to defaults?"))) {
				this.config.configure({reset:true});
				this.config.save();
			}
		}catch(e){log("WM.App.resetConfig: "+e);}};		
		
		//fetch posts only for this app
		//normally used for initial fetching only
		this.fetchPosts=function(){try{
			WM.fetch({bypassPause:true, apps:this});
		}catch(e){log("WM.App.fetchPosts: "+e);}};

		this.fetchNewer=function(){try{
			WM.fetch({
				newer:true,
				apps:this,
				bypassPause:true,
				bypassAppDisabled:true
			});
		}catch(e){log("WM.App.fetchNewer: "+e);}};

		this.fetchOlder=function(){try{
			WM.fetch({
				older:true,
				apps:this,
				bypassPause:true,
				bypassAppDisabled:true
			});
		}catch(e){log("WM.App.fetchOlder: "+e);}};

		//get a list of posts for this app from the global posts list
		this.__defineGetter__("posts",function(){try{
			return matchByParam(WM.posts,"app",this,"object");
		}catch(e){log("WM.App.getPosts: "+e);}});
		
		//detect if this sidekick said it was chrome compatible
		this.__defineGetter__("isVer3",function(){try{
			return this.flags.postMessageCompatible || this.flags.worksInChrome;
		}catch(e){log("WM.App.isVer3: "+e);}});

		//detect if is paused
		this.__defineGetter__("paused",function(){try{
			return this._paused;
		}catch(e){log("WM.App.paused: "+e);}});
		this.__defineSetter__("paused",function(v){try{
			this._paused=v;
			//update the sidekick page button graphics
			var btn=this.pauseButtonNode;
			if (btn) {
				var btnSize=WM.opts.littleButtonSize;
				with (btn.parentNode) 
					className=className.swapWordB(this._paused,"oddGreen","oddOrange");
				with (btn) 
					className=className.swapWordB(this._paused,"playRight"+btnSize,"pause"+btnSize);
			}
			//do events
			if (this._paused) WM.rulesManager.doEvent("onAppPaused",this);
			else WM.rulesManager.doEvent("onAppUnpaused",this);			
		}catch(e){log("WM.App.paused: "+e);}});
		
		//detect if is enabled
		this.__defineGetter__("enabled",function(){try{
			return this._enabled;
		}catch(e){log("WM.App.enabled: "+e);}});
		this.__defineSetter__("enabled",function(v){try{
			this._enabled=v;
			
			//update the WM.quickOpts
			WM.quickOpts.masterSwitch[this.appID]=this._enabled;
			WM.saveQuickOpts();
			
			//update the sidekick page graphics
			if (this.toggleNode) this.toggleNode.checked=this._enabled;
			if (this.node) with (this.node){
				className=className.swapWordB(this._enabled,"enabled","disabled");
			}
			
			//do events
			if (this._enabled) WM.rulesManager.doEvent("onAppEnabled",this);
			else WM.rulesManager.doEvent("onAppDisabled",this);
		}catch(e){log("WM.App.enabled: "+e);}});
				
		this.__defineGetter__("acceptCount",function(){try{
			return this._acceptCount;
		}catch(e){log("WM.App.acceptCount: "+e);}});
		this.__defineSetter__("acceptCount",function(v){try{
			this._acceptCount=v;
			if (this.acceptCounterNode) this.acceptCounterNode.textContent=v;
		}catch(e){log("WM.App.acceptCount: "+e);}});
		
		this.__defineGetter__("failCount",function(){try{
			return this._failCount;
		}catch(e){log("WM.App.failCount: "+e);}});
		this.__defineSetter__("failCount",function(v){try{
			this._failCount=v;
			if (this.failCounterNode) this.failCounterNode.textContent=v;
		}catch(e){log("WM.App.failCount: "+e);}});

		this.__defineGetter__("totalCount",function(){try{
			return this._failCount+this._acceptCount;
		}catch(e){log("WM.App.totalCount: "+e);}});

		//detect if this app is bundled with another app
		//return the main app in this bundle
		this.__defineGetter__("synApp",function(){try{
			return this.parent||this;
		}catch(e){log("WM.App.synApp: "+e);}});
		
		this.toggleContent=function(){try{
			this.expanded=!this.expanded;
			var btnSize=WM.opts.littleButtonSize;
			with (this.contentNode)
				className=className.swapWordB(this.expanded,"expanded","collapsed");
			with (this.toggleImgNode)
				className=className.swapWordB(this.expanded,"treeCollapse"+btnSize,"treeExpand"+btnSize);
		}catch(e){log("WM.App.toggleContent: "+e);}};

		this.showConfig=function(){try{
			this.config.open();
		}catch(e){log("WM.App.showConfig: "+e);}};

		this.disableOpt=function(w){try{
			this.opts[w]=false;
			this.config.set(w,false);
			this.config.save();
		}catch(e){log("WM.App.disableOpt: "+e);}};

		this.enableOpt=function(w){try{
			this.opts[w]=true;
			this.config.set(w,true);
			this.config.save();
		}catch(e){log("WM.App.enableOpt: "+e);}};
		
		//add menu elements
		try{
			/* no longer used in WM3
			if (this.menu) {
				//prefix all menu elements with the appID
				this.menu=WM.dock.fixMenu(this.menu,this.appID);
				//append this app's menu settings
				this.settingsBranch=WM.config.append({branch:"wmtab_games",data:this.menu});
			}
			//prefix all test returns with the appID
			WM.dock.fixTests(this.tests,this);
			//prefix all accept text id's with the appID
			WM.dock.fixAcceptTexts(this);
			*/
			
			//new method
			if (this.menu) this.config.append({data:this.menu});
			
			//I should really move these into the sidekick realm
			var data={}; 
			data["dynamic"+this.appID]=checkBox(this.name+" ("+this.appID+")",true);
			WM.config.append({branch:"enableDynamic",data:data});

			data={}; data[this.appID+"dontsteal"]=checkBox(this.name);
			WM.config.append({branch:"dontstealBlock",data:data});
			
			data={}; data["hide"+this.appID]=checkBox(this.name);
			WM.config.append({branch:"filterapps",data:data});
			
			data={}; data["nolike"+this.appID]=checkBox(this.name);
			WM.config.append({branch:"blockautolikebygame",data:data});
		} catch(e) {log("WM.App.init:addMenuElements: "+e);};
					
		//draw to #sidekickList (WM.console.sidekickNode)
		try{
			WM.console.sidekickNode.appendChild(
				this.node=createElement("div",{className:"listItem "+((this.enabled)?"enabled":"disabled")},[
					createElement("div",{className:"line"},[
						createElement("div",{className:"littleButton",title:"Toggle Content",onclick:function(){self.toggleContent();}},[
							this.toggleImgNode=createElement("img",{className:"resourceIcon "+(this.expanded?"treeCollapse"+WM.opts.littleButtonSize:"treeExpand"+WM.opts.littleButtonSize)}),
						]),
						this.toggleNode=createElement("input",{type:"checkbox",checked:this.enabled,onchange:function(){
							self.enabled=this.checked;
							with (self.node) className=className.toggleWordB(!this.checked,"disabled");
						}}),
						(this.icon)?createElement("img",{className:"icon crisp", src:this.icon,style:"width: 32px;vertical-align: middle"}):null,
						createElement("label",{textContent: this.name}),
						
						//toolbox
						createElement("div",{className:"littleButton odd"+(this.paused?"Green":"Orange"), title:"Pause/Unpause"},[
							this.pauseButtonNode=createElement("img",{className:"resourceIcon "+(this.paused?"playRight":"pause")+WM.opts.littleButtonSize,onclick:function(){self.paused=!self.paused;}})]),
						createElement("div",{className:"littleButton oddBlue", title:"Reset config for this app"},[
							createElement("img",{className:"resourceIcon uncheckAll"+WM.opts.littleButtonSize,onclick:function(){self.resetConfig();}})]),
						createElement("div",{className:"littleButton oddBlue", title:"Fetch Newer Posts"},[
							createElement("img",{className:"resourceIcon rssUpRight"+WM.opts.littleButtonSize,onclick:function(){self.fetchNewer();}})]),
						createElement("div",{className:"littleButton", title:"Fetch Older Posts"},[
							createElement("img",{className:"resourceIcon rssDownLeft" +WM.opts.littleButtonSize,onclick:function(){self.fetchOlder();}})]),
						
						//new sidekick config button
						this.configButton=createElement("button",{textContent:"Options", onclick:function(){self.config.open();}}),
					]),
					this.contentNode=createElement("div",{className:"subsection "+(this.expanded?"expanded":"collapsed")},[
						createElement("div",{className:"line"},[
							createElement("label",{textContent:"App ID:"}),
							createElement("span",{textContent:this.appID}),
						]),
						createElement("div",{className:"line"},[
							createElement("label",{textContent:"Support Provided By:"}),
							(this.desc)?createElement("span",{textContent: this.desc}):null, //provided in sidekick block
						]),
						createElement("div",{className:"line"},[
							createElement("label",{textContent:"Sidekick Help Link:"}),
							(this.helpLink)?createElement("a",{href:this.helpLink,textContent:this.helpLink}):null, //provided in sidekick block
						]),
						//browsers supported
						createElement("div",{className:"line"},[
							createElement("label",{textContent:"Browsers Supported:",style:"vertical-align:top;"}),
							createElement("img",{className:"resourceIcon firefox16", style:"display:inline-block;",title:"FireFox"}),
							(this.isVer3)?createElement("img",{className:"resourceIcon chrome16", style:"display:inline-block;",title:"Google Chrome"}):null,
						]),
						//types paused subbox
						createElement("div",{className:"line"},[
							createElement("label",{textContent:"Types Paused:",title:"This is a list of bonus types that are currently paused for this app."}),
							createElement("div",{className:"littleButton oddGreen",onclick:function(){self.unpauseAllTypes();},title:"Unpause all types by this app."},[
								createElement("img",{className:"resourceIcon playRight"+WM.opts.littleButtonSize}),
							]),
							this.typesPausedNode=createElement("div",{className:"subsection"}),
						]),				
						//attached apps
						createElement("div",{className:"line"},[
							createElement("label",{textContent:"Attached Apps:",title:"Additional apps filtered and processed by this sidekick."}),
							this.filtersNode=createElement("div",{className:"subsection"}),
						]),
						//helpers subbox
						createElement("div",{className:"line"},[
							createElement("label",{textContent:"Helpers:",title:"Sidekick helpers"}),
							this.helpersNode=createElement("div",{className:"subsection"}),
						]),
						//user defined types subbox
						createElement("div",{className:"line"},[
							createElement("label",{textContent:"User-Defined Types:",title:"User Defined Types ('which')"}),
							createElement("div",{className:"littleButton oddGreen",onclick:function(){self.addUDT();},title:"Add New User Defined Type"},[
								createElement("img",{className:"resourceIcon plus"+WM.opts.littleButtonSize}),
							]),
							this.udtNode=createElement("div",{className:"subsection"}),
						]),
					]),
				])
			);
		}catch(e){log("WM.App.init:addSidekickElement: "+e);};
		
		//create feed filters for this app
		try{
			var feeds=WM.feedManager.feeds;
			for (var f=0,len=feeds.length;f<len;f++){
				feeds[f].addFilter({id:"app_"+this.appID});
			}
		}catch(e){log("WM.App.init:createFeedFilters: ")+e;}

		//draw to collection filter coolbar
		try{
			//create game filter buttons on the WM.console
			var coolBar = WM.console.collectTabControl;
			if (coolBar) {
				//add a tab for this filter
				var tab = coolBar.addTab({
					text:(this.name||""),
					image:(this.icon||null),
					appFilter:this.appID,
					onSelect:WM.setAppFilter,
					selected:(WM.quickOpts.filterApp==this.appID),
				});
				this.collectionTabNode=tab.buttonNode;
				
				//force the image to have the 'crisp' drawing style
				tab.buttonNode.childNodes[0].className="icon crisp";
				
				//add accept/fail counters
				this.failCount=0;
				this.acceptCount=0;
				tab.buttonNode.insertBefore(
					createElement("div",{className:"accFailBlock"},[
						this.failCounterNode=createElement("span",{className:"fail",textContent:"0"}),
						this.acceptCounterNode=createElement("span",{className:"accept",textContent:"0"}),
					])
				, tab.textNode);
			}
		} catch(e) {log("WM.App.init:addConsoleElement: "+e);};
		
		//show additional filtered apps
		try{
			if (isArrayAndNotEmpty(this.addFilters)) {
				for (var f,filt;(filt=this.addFilters[f]);f++){
					//create an app object for this filter
					filt.parent=this;
					this.kids.push(new WM.App(filt));
					if (this.filtersNode) this.filtersNode.appendChild(
						createElement("div",{className:"line"},[
							createElement("img",{className:"icon crisp", src:filt.icon||null}),
							createElement("text",filt.name),
						])
					);
				}
			}
		} catch(e) {log("WM.App.init:addFilteredApps: "+e);};
		
		//draw my user defined types
		try{
			for (var u in this.userDefinedTypes){
				this.addUDT({id:u,name:this.userDefinedTypes[u]},true);
			}
		}catch(e){log("WM.App.init: drawUDTs: "+e);}
		
		//do events
		WM.rulesManager.doEvent("onSidekickReady",this);
		
		return self;
	}catch(e){log("WM.App.init: "+e);}};

//***************************************************************************************************************************************
//***** Post Class
//***************************************************************************************************************************************
	WM.Post = function(params){try{
		this.objType="post";
		var self=this;
		params=params||{};

		//set defaults
		this.state=""; //classnames
		this.flags=0; //similar to classnames
		this.node=null; //collector panel node
		this.originalData=JSON.stringify(params); //clone the original data from facebook for viewing later
		
		//convert FQL data to what we have previously expected from graph api		
		this.id=params.post_id;
		this.app=WM.apps[params.app_id];
		this.fromID=params.source_id.toString()||this.id.split("_")[0];
		this.permalink="http://www.facebook.com/"+this.id.split("_")[0]+"/posts/"+this.id.split("_")[1];
		this.fromName=params.fromName;
		this.date=params.created_time;
		this.message=params.message;
		this.name=params.attachment.name;
		this.title=params.name;
		this.caption=params.attachment.caption;
		this.description=params.attachment.description;
		this.picture=params.app_data.images;
		try{
			this.picture = JSON.parse(this.picture)[0].fbml.match(/https?\:.+\.(png|gif|jpg)/)[0];
		} catch(e){
			//picture data match failed, no big deal
			//leave the data messed up because rules manager might still be able to use it
		}
		this.linkHref=(params.action_links||null)?params.action_links[0].href:(params.attachment.media||null)?params.attachment.media[0].href:"";
		this.linkText=(params.action_links||null)?params.action_links[0].text:(params.attachment.media||null)?params.attachment.media[0].alt:"";
		this.targetID=params.target_id;
		this.isLiked=params.like_info.user_likes;
		
		//convert a unix date to a readable date
		this.realtime=(new Date(this.date*1000).toLocaleString());
		
		//set a timer on the post for delayed deletion
		this.drawTime=timeStamp();
		
		this._isLiked=false;
		this._isPinned=false;
		this._isPaused=false;
		this._isScam=false;
		this._isW2W=false;
		this._isForMe=false;
		this._isMyPost=false;
		this._isWishlist=false;
		this._isUndefined=false;
		this._status=0;
		this._isTimeout=false;
		this._isFailed=false;
		this._isAccepted=false;
		this._isExcluded=false;
		this._isStale=false;
		this._isCollect=false;
		this._isWorking=false;
		this._which=null;
		this._idText="";
		
		//use passed params
		//for (var p in params) this[p]=params[p];

		//link to our application array of objects
		//this.app=WM.apps[this.application.id];

		//shortcuts to app details
		this.__defineGetter__("synApp",function(){try{
			return this.app.synApp;
		}catch(e){log("WM.Post.synApp: "+e);}});

		this.__defineGetter__("postedDay",function(){try{
			var d=new Date(this.date*1000);
			return d.getFullYear()+"/"+d.getMonth()+"/"+d.getDay();
		}catch(e){log("WM.Post.postedDay: "+e);}});

		this.__defineGetter__("postedHour",function(){try{
			var d=new Date(this.date*1000);
			var h=d.getHours();
			var pm=(h/12)>1;
			return d.getFullYear()+"/"+d.getMonth()+"/"+d.getDay()+"   "+((h>12)?h-12:h)+":00"+((pm)?"PM":"AM");
		}catch(e){log("WM.Post.postedHour: "+e);}});

		this.__defineGetter__("appID",function(){try{
			return this.app.appID;
		}catch(e){log("WM.Post.appID: "+e);}});

		this.__defineGetter__("appName",function(){try{
			return this.app.name;
		}catch(e){log("WM.Post.appName: "+e);}});

		//get/set priority
		this.__defineGetter__("priority",function(){try{
			return this._priority;
		}catch(e){log("WM.Post.priority: "+e);}});
		this.__defineSetter__("priority",function(v){try{
			this._priority=v;
		}catch(e){log("WM.Post.priority: "+e);}});

		//get/set liked status
		this.__defineGetter__("isLiked",function(){try{
			return this._isLiked;
		}catch(e){log("WM.Post.isLiked: "+e);}});
		this.__defineSetter__("isLiked",function(v){try{
			this._isLiked=v;
			//remove the toolbutton if liked
			if (this.node) with (this.node) 
				className=className.toggleWordB(this._isLiked,"liked");
			if (this.likeButtonNode) with (this.likeButtonNode)
				className=className.toggleWordB(this._isLiked,"hidden");
		}catch(e){log("WM.Post.isLiked: "+e);}});

		//identification flags
		this.__defineGetter__("isScam",function(){try{
			return this._isScam;
		}catch(e){log("WM.Post.isScam: "+e);}});
		this.__defineSetter__("isScam",function(v){try{
			this._isScam=v;
			if (this.node) with (this.node) 
				className=className.toggleWordB(this._isScam,"scam");
		}catch(e){log("WM.Post.isScam: "+e);}});

		this.__defineGetter__("isMyPost",function(){try{
			return this._isMyPost;
		}catch(e){log("WM.Post.isMyPost: "+e);}});
		this.__defineSetter__("isMyPost",function(v){try{
			this._isMyPost=v;
			if (this.node) with (this.node) 
				className=className.toggleWordB(this._isMyPost,"isMyPost");
		}catch(e){log("WM.Post.isMyPost: "+e);}});

		this.__defineGetter__("isW2W",function(){try{
			return this._isW2W;
		}catch(e){log("WM.Post.isW2W: "+e);}});
		this.__defineSetter__("isW2W",function(v){try{
			this._isW2W=v;
			if (this.node) with (this.node) 
				className=className.toggleWordB(this._isW2W,"w2w");
		}catch(e){log("WM.Post.isW2W: "+e);}});

		this.__defineGetter__("isForMe",function(){try{
			return this._isForMe;
		}catch(e){log("WM.Post.isForMe: "+e);}});
		this.__defineSetter__("isForMe",function(v){try{
			this._isForMe=v;
			if (this.node) with (this.node) 
				className=className.toggleWordB(this._isForMe,"isForMe");
		}catch(e){log("WM.Post.isForMe: "+e);}});

		this.__defineGetter__("isWishlist",function(){try{
			return this._isWishlist;
		}catch(e){log("WM.Post.isWishlist: "+e);}});
		this.__defineSetter__("isWishlist",function(v){try{
			this._isWishlist=v;
			if (this.node) with (this.node) 
				className=className.toggleWordB(this._isWishlist,"wishlist");
		}catch(e){log("WM.Post.isWishlist: "+e);}});

		this.__defineGetter__("isUndefined",function(){try{
			return this._isUndefined;
		}catch(e){log("WM.Post.isUndefined: "+e);}});
		this.__defineSetter__("isUndefined",function(v){try{
			this._isUndefined=v;
			if (this.node) with (this.node) 
				className=className.toggleWordB(this._isUndefined,"noDef");
		}catch(e){log("WM.Post.isUndefined: "+e);}});

		this.__defineGetter__("isStale",function(){try{
			return this._isStale;
		}catch(e){log("WM.Post.isStale: "+e);}});
		this.__defineSetter__("isStale",function(v){try{
			this._isStale=v;
			if (this.node) with (this.node) 
				className=className.toggleWordB(this._isStale,"stale");
		}catch(e){log("WM.Post.isStale: "+e);}});

		this.__defineGetter__("isTimeout",function(){try{
			return this._isTimeout;
		}catch(e){log("WM.Post.isTimeout: "+e);}});
		this.__defineSetter__("isTimeout",function(v){try{
			this._isTimeout=v;
			if (this.node) with (this.node) 
				className=className.toggleWordB(this._isTimeout,"timeout");
		}catch(e){log("WM.Post.isTimeout: "+e);}});

		this.__defineGetter__("isCollect",function(){try{
			return this._isCollect;
		}catch(e){log("WM.Post.isCollect: "+e);}});
		this.__defineSetter__("isCollect",function(v){try{
			this._isCollect=v;
			if (this.node) with (this.node) 
				className=className.toggleWordB(this._isCollect,"collect");
		}catch(e){log("WM.Post.isCollect: "+e);}});

		this.__defineGetter__("isExcluded",function(){try{
			return this._isExcluded;
		}catch(e){log("WM.Post.isExcluded: "+e);}});
		this.__defineSetter__("isExcluded",function(v){try{
			this._isExcluded=v;
			if (this.node) with (this.node) 
				className=className.toggleWordB(this._isExcluded,"excluded");
		}catch(e){log("WM.Post.isExcluded: "+e);}});

		this.__defineGetter__("isAccepted",function(){try{
			return this._isAccepted;
		}catch(e){log("WM.Post.isAccepted: "+e);}});
		this.__defineSetter__("isAccepted",function(v){try{
			this._isAccepted=v;
			if (this.node) with (this.node) 
				className=className.toggleWordB(this._isAccepted,"accepted");
		}catch(e){log("WM.Post.isAccepted: "+e);}});

		this.__defineGetter__("isFailed",function(){try{
			return this._isFailed;
		}catch(e){log("WM.Post.isFailed: "+e);}});
		this.__defineSetter__("isFailed",function(v){try{
			this._isFailed=v;
			if (this.node) with (this.node) 
				className=className.toggleWordB(this._isFailed,"failed");
		}catch(e){log("WM.Post.isFailed: "+e);}});

		this.__defineGetter__("isWorking",function(){try{
			return this._isWorking;
		}catch(e){log("WM.Post.isWorking: "+e);}});
		this.__defineSetter__("isWorking",function(v){try{
			this._isWorking=v;
			if (this.node) with (this.node) 
				className=className.toggleWordB(this._isWorking,"working");
		}catch(e){log("WM.Post.isWorking: "+e);}});

		this.__defineGetter__("isColored",function(){try{
			return this._isColored;
		}catch(e){log("WM.Post.isColored: "+e);}});
		this.__defineSetter__("isColored",function(v){try{
			this._isColored=v;
			if (this._isColored && this.colorOverride && this.node) this.node.style.setProperty("background-color",this.colorOverride,"important");
		}catch(e){log("WM.Post.isColored: "+e);}});
		
		//get/set post pinned
		this.__defineGetter__("isPinned",function(){try{
			return this._isPinned;
		}catch(e){log("WM.Post.isPinned: "+e);}});
		this.__defineSetter__("isPinned",function(v){try{
			this._isPinned=v;
			//rotate the pin icon
			var btnSize=WM.opts.littleButtonSize;
			if (this.pinImageNode) with (this.pinImageNode) 
				className=className.swapWordB(this._isPinned,"pinned"+btnSize,"pin"+btnSize);
			//pinned class
			if (this.node) with (this.node) 
				className=className.toggleWordB(this._isPinned,"pinned");
		}catch(e){log("WM.Post.isPinned: "+e);}});

		//get/set post paused
		this.__defineGetter__("isPaused",function(){try{
			return this._isPaused;
		}catch(e){log("WM.Post.isPaused: "+e);}});
		this.__defineSetter__("isPaused",function(v){try{
			this._isPaused=v;
			if (this.node) with (this.node) 
				className=className.toggleWordB(this._isPaused,"paused");
		}catch(e){log("WM.Post.isPaused: "+e);}});

		//get/set status
		this.__defineGetter__("status",function(){try{
			return this._status;
		}catch(e){log("WM.Post.status: "+e);}});
		this.__defineSetter__("status",function(v){try{
			this._status=v;
			if (this.statusTextNode) this.statusTextNode.textContent=this._status;
		}catch(e){log("WM.Post.status: "+e);}});

		//get/set idText
		this.__defineGetter__("idText",function(){try{
			return this._idText;
		}catch(e){log("WM.Post.idText: "+e);}});
		this.__defineSetter__("idText",function(v){try{
			this._idText=v;
			if (this.linkNode) this.linkNode.textContent=((this._idText||null) && WM.opts.debugrecog)?this._idText:this.linkText;
		}catch(e){log("WM.Post.idText: "+e);}});
		
		//get/set which bonus type this is
		this.__defineGetter__("which",function(){try{
			return this._which;
		}catch(e){log("WM.Post.which: "+e);}});
		this.__defineSetter__("which",function(v){try{
			this._which=v;
			if (this.whichTextNode) this.whichTextNode.textContent=this._which;
		}catch(e){log("WM.Post.which: "+e);}});

		//check if in history already
		this.__defineGetter__("alreadyProcessed",function(){try{
			return exists(WM.history[this.id]);
		}catch(e){log("WM.Post.alreadyProcessed: "+e);}});
		
		/*
		//update the namespace parameter if it does not exist
		if (!exists(this.app.namespace)) this.app.namespace=this.application.namespace;

		//validate the application namespace for sidekicks that provide namespace checking
		if (exists(this.app.namespace) && (this.app.namespace!=this.application.namespace)) {
			//Graph API namespace does not match sidekick known namespace, flag as scam
			this.isScam=true; 
		}		

		//now drop the application object we got from FB
		if (exists(this.application)) delete this.application;
		*/
		
		//this.fromID=this.from.id;
		//this.fromName=this.from.name;
		this.fromNameLastFirst=this.fromName;
		var sp=this.fromName.split(" ");
		if (isArray(sp) && sp.length>1) {
			this.fromNameLastFirst = sp.pop()+", "+sp.join(" ");
		}


		
		//(re)identify this post
		this.identify=function(params){try{
			params=params||{};
			
			//shortcuts
			var post=this;
			var app=post.app;
			var synApp=app.synApp;
		
			//set/reset priority, state, status & flags
			this.priority=50;
			this.status=0;
			this.state="";
			
			//prevent reset of some data holders
			if (!params.reid) {
				this.testData={};
				this.isLiked=false;
				this.isMyPost=false;
				this.isW2W=false;
				this.isForMe=false;
				this.isScam=false;
			}
			
			//reset data holders
			this.isStale=false;
			this.isCollect=false;
			this.isExcluded=false;
			this.isFailed=false;
			this.isAccepted=false;
			this.isPaused=false;
			this.isPinned=false;
			this.isUndefined=false;
			this.isWishlist=false;
			this.isTimeout=false;
			
			//avoid posts that belong to a disabled sidekick
			if(!WM.quickOpts.masterSwitch[app.appID]) {
				//master switch is off
				this.isExcluded=true;
				return true; //draw without identifying anything
			}

			//hide posts by apps that we specifically told to hide
			if (WM.opts["hide"+app.appID]) {this.remove(); return false;}

			//avoid potential scam posts
			/*if (WM.opts.scamblock) {
				if (!params.reid) {
					this.isScam=(!this.linkHref.match(new RegExp("^http(s):\/\/apps\.facebook\.com\/"+app.namespace))!=null);
				}
				if (this.isScam){
					this.isExcluded=true;
					if (WM.opts.hidescams) {this.remove(); return false;}
				}
			}*/

			//avoid posts by self
			if (!params.reid) {
				var whoPosted = this.fromID;
				var whoName = this.fromName;
				this.isMyPost=(whoPosted==WM.currentUser.id);
			}
			if (this.isMyPost){
				this.isExcluded=true;
				if (WM.opts.hidemyposts) {this.remove(); return false;}
			}

			//avoid W2W posts not for me
			if (!params.reid){
				this.isForMe = (this.targetID==WM.currentUser.id);
				this.isW2W = (this.targetID||null);
			}
			if (WM.opts[app.appID+"dontsteal"] && this.isW2W && !this.isForMe){
				this.isExcluded=true;
				if (WM.opts.hidenotforme) {this.remove(); return false;}
			}

			//avoid posts older than olderLimit
			if (olderLimit!=0) {
				if (this.isStale=this.checkStale(olderLimit)){
					if (WM.opts.skipstale) this.isExcluded=true;
					if (WM.opts.hidestale) {this.remove(); return false;}
				}
			}

			//get bonus type
			var w=(this.which = WM.which(this,{reid:params.reid}));

			//check for exclude type
			if (w=="exclude") {
				this.isExcluded=true;
			}

			//check for pause
			if(synApp.typesPaused.inArray(w)) this.isPaused=true;
			
			//check if undefined
			if (w=="none") {
				this.isUndefined=true;
			}
			if (w==synApp.appID+"doUnknown" || w==synApp.appID+"send") {
				this.isUndefined=true;
			}

			//special pin undefined option
			if (WM.opts.pinundefined && this.isUndefined) this.isPinned=true;

			//check if liked by me		
			if (this.isLiked){
				if (WM.opts.skipliked){
					if (WM.opts.markliked) this.status=1; //mark liked as accepted
					this.isExcluded=true;
				}
				if (WM.opts.hideliked) {this.remove(); return false;}
			}

			//check history
			this.status=this.status||0;
			if (this.alreadyProcessed) {
				//post previously processed
				this.status=(WM.history[this.id].status||0);

				var gotItem=((this.status>0) || (this.status==-6) || (this.status==-4) || (this.status==-15 && WM.opts.accepton15));
				if (gotItem) {
					this.isAccepted=true;
				} else if (this.status<0) {
					this.isFailed=true;
				}

				if (WM.opts.hideaccepted && gotItem) {this.remove(); return false;}
				if (WM.opts.hidefailed && this.status<0) {this.remove(); return false;}
			}

			//check if excluded
			if (this.isExcluded && WM.opts.hideexcluded) {this.remove(); return false;}

			//set identified text
			this.idText=WM.getAccText(synApp.appID,w,(this.alreadyProcessed),this.status);

			//check if wanted
			this.isCollect=(!this.alreadyProcessed && 
				(w=="dynamic" || WM.apps[this.synApp.appID].opts[w] || 
					(w.startsWith("send") && WM.apps[this.synApp.appID].opts["sendall"])
				)
			);

			//check if wishlist
			if (w.find("wishlist")) {
				this.isWishlist=true;
				if (WM.opts.hideunwanted && !WM.opts.donthidewishlists) {this.remove(); return false;}
			}
			
			//if unwanted
			if (!this.isCollect && WM.opts.hideunwanted) {this.remove(); return false;}
			
			//debug post
			/*var pkg=debug.print("WM.Post.debug: ");
			pkg.msg.appendChild(createElement("button",{type:"button",onclick:function(){
					//response.responseText.toClipboard();
					promptText(JSON.stringify(self));
				}},[
				createElement("img",{src:"http://i1181.photobucket.com/albums/x430/merricksdad/array.png",title:"Show Data",style:"width:16px;height:16px; vertical-align:bottom;"})
			]));*/
			
			//return true to draw, false to hide
			return true;
			
		}catch(e){log("WM.Post.identify: "+e);}};

		//open this post using the collector system
		this.open=function(params){try{
			params=params||{};
			var post = this;
			var id = this.id;
			var app = this.app;
			var synApp = this.synApp;

			//perform the onBefore Collect event
			WM.rulesManager.doEvent("onBeforeCollect",post);

			//fix the link based on sidekick alterlink information
			var alterLink=(synApp.alterLink||null);
			var targetHref = post.linkHref;
			var doAlterLink=(synApp.flags.alterLink||false);
			if (doAlterLink && alterLink) {
				//alert("doing alterlink...");
				//pack the alterlink into an array, or detect an array
				if (!isArray(alterLink)) alterLink=[alterLink];
				
				//iterate link alteration commands
				for (var alt=0,alteration;(alteration=alterLink[alt]);alt++) {
				
					//alert("making alteration...");
							
					//note that only find and replace functionality is available right now, no wildcards or array inserts will work
					var find = (alteration.find||"");
					alteration.dataSource=(alteration.dataSource||"either");
					
					//check if user is wanting a regex or string replacement
					if (alteration.isRegex||false) find=new RegExp(find,"");
										
					targetHref = targetHref.replace(find,(alteration.replace||""));


					//check for word specific changes
					if ((alteration.words||null) && (alteration.conversionChart||null)){
						//alert("inserting words...");
						
						//new alterlink capability to change data source from 'either' to another post part
						var dataSource = post.testData[alteration.dataSource].toLowerCase();
						
						//alert(dataSource);
						
						for (var w=0,len=alteration.words.length; w<len; w++) {
							var word=(alteration.words[w]).toLowerCase();
							if (dataSource.contains(word)) {
								//replace the word
								targetHref=targetHref.replace("{%1}",alteration.conversionChart[word.noSpaces()]);
								break;
							}
						}
					}
				
				}
			}

			//fix the link, removing https and switching to http only
			targetHref = targetHref.replace('https://','http://');

			//open the bonus page in a new tab or the previously opened tab object to save memory
			this.isWorking=true;
			post.state="working";
			WM.requestsOpen++;
			doAction(function(){WM.collector.open({url:targetHref,id:id,callback:(synApp.isVer3?WM.onFrameLoad3:WM.onFrameLoad),post:post,first:params.first||false,emergency:params.emergency||false});});
		}catch(e){log("WM.Post.open: "+e);}};

		//open this post using the collector system even if already tried
		this.forceOpen=function(params){try{
			var post=self;
			this.isCollect=true;
			this.isFailed=false;
			this.isTimeout=false;
			this.isAccepted=false;
			post.open(params);
		}catch(e){log("WM.Post.forceOpen: "+e);}};

		//like this post using the collector system
		this.like=function(){try{
			//var url=this.permalink;
			var self=this;
			//setTimeout(function(){WM.collector.open({url:url+"#likeit=true",id:url,callback:WM.onLikePost,post:self});},100);
			//Graph.likePost(this.id,{callback:WM.onLikePost,post:self});
			setTimeout(function(){Graph.likePost(self.id,{callback:WM.onLikePost,post:self});},100);
		}catch(e){log("WM.Post.like: "+e);}};

		//comment on this post using the collector system
		this.comment=function(commentOverride){try{
			if (commentOverride=="") commentOverride = null;
			//not ready
			//confirm("Feature not ready");
			//return;
			//var url=this.permalink;
			var self=this;
			var say = commentOverride || WM.opts.autocommentlist.split("\n").pickRandom();
			//setTimeout(function(){WM.collector.open({url:url+"#commentit=true&say="+say,id:url,callback:WM.onLikePost,post:self});},100);
			log("commenting "+say);
			//Graph.commentPost(this.id,say);
			setTimeout(function(){Graph.commentPost(self.id,say);},100);
		}catch(e){log("WM.Post.comment: "+e);}};

		//cancel collection in progress
		this.cancelProcess=function(){
			//tell the collector to cancel any processes with post equal this
			//this will cancel both collect and like activities
			WM.collector.cancelProcess({search:"post",find:this});
			this.processCancelled=true;
		},

		//cancel collection in progress
		this.refreshProcess=function(){
			//tell the collector to reload the href on processes with post equal this
			//this will reload both collect and like activities
			WM.collector.refreshProcess({search:"post",find:this});
			this.processRestarted=true;
		},

		//change the background color of this post
		this.setColor=function(color){try{
			this.colorOverride=color;
			this.isColored=(cBool(color));
		}catch(e){log("WM.Post.setColor: "+e);}};
		
		//change the bonus type of this post
		//and mark it for collection if needed
		//and update its idText
		this.setWhich=function(w){try{
			this.which=w;
			if ((w=="dynamic") || WM.apps[this.synApp.appID].opts[w] || (w.startsWith("send") && WM.apps[this.synApp.appID].opts["sendall"]) ) {
				this.isCollect=!this.alreadyProcessed;
			}
			//update the identified text
			this.idText=WM.getAccText(this.synApp.appID,w,(this.alreadyProcessed),this.status);
		}catch(e){log("WM.Post.setWhich: "+e);}};
		
		//get the time passed since this post was created
		this.__defineGetter__("age",function(){try{
			return timeStamp()-(this.date*1000);
		}catch(e){log("WM.Post.age: "+e);}});

		this.__defineGetter__("whichText",function(){try{
			if (this.which=="dynamic") return "Dynamic Grab";
			return this.synApp.userDefinedTypes[this.which]||this.synApp.accText[this.which];
		}catch(e){log("WM.Post.whichText: "+e);}});

		this.draw=function(redraw,reorder){try{
			var post=this;
			var app=post.app;
			var synApp=app.synApp;
			
			//clean old display
			if (this.node && redraw) {
				remove(this.node);
				this.node=null;
			}

			//prefetch css words
			var tags=("")
				.toggleWordB(post.isAccepted,"accepted")
				.toggleWordB(post.isFailed,"failed")
				.toggleWordB(post.isTimeout,"timeout")
				.toggleWordB(post.isExcluded,"excluded")
				.toggleWordB(post.isStale,"stale")
				.toggleWordB(post.isCollect && !(post.isAccepted||post.isFailed),"collect")
				.toggleWordB(post.isWorking,"working")
				.toggleWordB(post.isW2W,"w2w")
				.toggleWordB(post.isForMe,"isForMe")
				.toggleWordB(post.isMyPost,"isMyPost")
				.toggleWordB(post.isColored,"colored")
				.toggleWordB(post.isPaused,"paused")
				.toggleWordB(post.isPinned,"pinned")
				.toggleWordB(post.isUndefined,"noDef")
				.toggleWordB(post.isWishlist,"wishlist")
				.toggleWordB(post.isScam,"scam")
				.toggleWordB(post.isLiked,"liked");
									
			//detect hidden/drawn image
			var hideimage = (WM.opts.hideimages || (WM.opts.hideimagesunwanted && (post.which==="none" || post.which==="exclude") ) );
			var fakeimage = hideimage && WM.quickOpts.displayMode!="0";
			hideimage=hideimage && WM.quickOpts.displayMode=="0";
			
			//shared elements
			if (redraw){
				post.toolboxNode=createElement("div",{className:"toolBox small inline"});
				post.imageNode=createElement("img",{className:((!fakeimage && post.picture)?"":"resourceIcon noImageSmall16"),src:((!fakeimage)?post.picture:""||""),onerror:function(){this.className=this.className+" resourceIcon noImageSmall16"}});
				post.imageLinkNode=(!hideimage)?
					createElement("span",{href:jsVoid,className:"picture",onclick:function(){post.forceOpen();} },[
						post.imageNode
					]):null;
				post.floaterNode=null;
				post.bodyNode=null;
				post.actorNode=createElement("a",{className:"actor",textContent:post.fromName,href:"http://www.facebook.com/profile.php?id="+post.fromID});
				post.titleNode=createElement("span",{className:"title",textContent:post.name});
				post.captionNode=createElement("span",{className:"caption",textContent:post.caption});
				post.descriptionNode=createElement("span",{className:"description",textContent:post.description});
				post.dateNode=createElement("span",{className:"postDate",textContent:[post.date,post.realtime]});
				post.viaNode=createElement("a",{className:"appName",textContent:"  via "+app.name,href:"http://apps.facebook.com/"+app.namespace+"/",title:app.appID});
				post.linkNode=createElement("a",{className:"linkText"+(post.isExcluded?" excluded":"")+(post.idText?" identified":""),textContent:((post.idText||null) && WM.opts.debugrecog)?post.idText:post.linkText,href:post.linkHref,title:post.linkText});
				post.statusNode=createElement("span",{className:"status",textContent:"Status: "+(post.status||"0")+ " " + (WM.statusText[post.status||"0"])});
				post.pausedNode=createElement("div",{className:"pausedHover",title:"Collection for this post is paused, click to reactivate.",onclick:function(){post.isPaused=false;}},[createElement("img",{className:"resourceIcon playRight64"})]);
				
				//create the layout
				switch (WM.quickOpts.displayMode||"0"){

					case "0": //classic mode
						post.node=createElement("div",{id:"post_"+post.id,className:"wm post classic "+tags+((hideimage)?" noimage":""),title:(post.isScam?"Post is possible scam":"")},[
							post.toolboxNode,
							post.actorNode,
							post.imageLinkNode,
							(!WM.opts.hidebody)?post.bodyNode=createElement("div",{className:"body"},[
								post.titleNode,
								(post.caption||null)?post.captionNode:null,
								(post.description||null)?post.descriptionNode:null,
							]):null,
							createElement("div",{style:"margin-top:5px;"},[
								(!WM.opts.hidedate)?post.dateNode:null,
								(!WM.opts.hidevia)?post.viaNode:null,
								post.linkNode,
							]),
							post.pausedNode,
						]);
						break;

					case "1": case "3": //short mode and old priority mode
						post.node=createElement("div",{id:"post_"+post.id,className:"wm post short "+WM.opts.thumbsize+tags,title:(post.isScam?"Post is possible scam":"")},[
							post.imageLinkNode,
							post.floaterNode=createElement("div",{id:"floater_"+post.id,className:"floater "+WM.opts.thumbsize},[
								post.toolboxNode,
								post.actorNode,
								post.dateNode,
								post.viaNode,
								post.linkNode,
								post.statusNode,
								post.pausedNode,
							]),
						]);
						post.imageNode.onmousemove=WM.moveFloater;
						break;

					case "2": //dev mode
						var fnLine=function(label,text){
							return createElement("div",{className:"line"},[
								createElement("label",{textContent:label+": "}),
								createElement("span",{textContent:text})
							]);
						};
						post.node=createElement("div",{id:"post_"+post.id,className:"listItem wm post dev "+tags,title:(post.isScam?"Post is possible scam":"")},[
							post.idNode=fnLine("id", post.id),
							post.toolboxNode,
							//post.imageLinkNode,
							createElement("div",{className:"subsection"},(function(){
								var ret = [];
								ret.push(post.actorNode=fnLine("fromName (fromID)", post.fromName+"("+post.fromID+")"));
								ret.push(post.titleNode=fnLine("title",post.name));
								if (post.message||null)ret.push(post.messageNode=fnLine("msg",post.message));
								if (post.caption||null)ret.push(post.captionNode=fnLine("caption",post.caption)); 
								if (post.description||null)ret.push(post.descriptionNode=fnLine("desc",post.description)); 
								ret.push(post.appImageNode=fnLine("img",post.picture));
								ret.push(post.dateNode=fnLine("date",post.realtime));
								ret.push(post.appNameNode=fnLine("appName",app.name));
								ret.push(post.appIDNode=fnLine("appID",app.appID));
								ret.push(post.canvasNode=fnLine("canvas",app.namespace));
								ret.push(post.urlNode=fnLine("url",post.linkHref));

								//show likes
								if (post.likes||null){
									if (post.likes.data||null){
										ret.push(fnLine("likes",""));
										ret.push(post.likesNode=createElement("div",{className:"subsection"},(function(){
											var data=post.likes.data;
											var retData=[];
											for(var d=0,lenL=data.length; d<lenL; d++){
												retData.push(fnLine("likeName(likeID)",data[d].name+"("+data[d].id+")"));
											}
											return retData;
										})()));
									}
								}

								//show comments
								if (post.comments||null){
									if (post.comments.data||null){
										ret.push(fnLine("comments",""));
										ret.push(post.commentsNode=createElement("div",{className:"subsection"},(function(){
											var data=post.comments.data;
											var retData=[];
											for(var d=0,lenC=data.length; d<lenC; d++){
												retData.push(fnLine("commentorName(commentorID)",data[d].from.name+"("+data[d].from.id+")"));
												retData.push(fnLine("comment",data[d].message));
											}
											return retData;
										})()));
									}
								}
								ret.push(post.idTextNode=fnLine("idText",post.idText));
								ret.push(post.whichNode=fnLine("which",post.which));
								ret.push(post.linkTextNode=fnLine("linkText",post.linkText));

								return ret;
							})() ),
							post.pausedNode
						]);
						break;
				}

				//add the toolbox
				post.addToolBox();
			
				//use color override
				if (post.colorOverride) {
					post.node.style.setProperty("background-color",post.colorOverride,"important");
				}
			}

			//if a filter exists check against filter
			var filter=(WM.quickOpts.filterApp||"All");
			if (filter!="All" && filter!=app.appID) {
				//dont show this post in this filter
				if (this.node) remove(this.node);
				return;
			}

			//insert the post into view by sort order
			if (redraw || reorder) {
				var groupBy=WM.quickOpts.groupBy;
				if (groupBy){
					//detect/create group
					var group=WM.newGroup({by:post[groupBy]});
					var sibling=post.nextSibling();
					if (sibling) group.insertBefore(post.node,sibling.node);
					else group.appendChild(post.node);
				} else {
					var sibling=post.nextSibling();
					if (sibling) WM.console.feedNode.insertBefore(post.node, sibling.node);
					else WM.console.feedNode.appendChild(post.node);
				}
			}
		}catch(e){log("WM.Post.draw: "+e);}};

		this.openSource=function(){try{
			var url=this.permalink;
			//FF22 version
			GM_openInTab(url,"_blank");
			//FF21 version
			//((WM.opts.useGM_openInTab)?GM_openInTab:window.open)(url,"_blank");
		}catch(e){log("WM.Post.openSource: "+e);}};

		this.addClass=function(s){try{
			if (this.node){
				this.node.className=this.node.className.addWord(s);
			}
		}catch(e){log("WM.Post.addWord: "+e);}};

		this.removeClass=function(s){try{
			if (this.node){
				this.node.className=this.node.className.removeWord(s);
			}
		}catch(e){log("WM.Post.removeWord: "+e);}};

		this.pause=function(){try{
			this.isPaused=true;
		}catch(e){log("WM.Post.pause: "+e);}};

		this.unPause=function(){try{
			this.isPaused=false;
		}catch(e){log("WM.Post.unPause: "+e);}};

		this.exclude=function(){try{
			this.isExcluded=true;
		}catch(e){log("WM.Post.exclude: "+e);}};

		this.collect=function(){try{
			this.isCollect=true;
		}catch(e){log("WM.Post.collect: "+e);}};

		this.stopCollect=function(){try{
			this.isCollect=false;
		}catch(e){log("WM.Post.collect: "+e);}};

		this.togglePin=function(){try{
			this.isPinned=!this.isPinned;
		}catch(e){log("WM.Post.togglePin: "+e);}};

		this.pin=function(){try{
			this.isPinned=true;
		}catch(e){log("WM.Post.pin: "+e);}};

		this.unPin=function(){try{
			this.isPinned=false;
		}catch(e){log("WM.Post.unPin: "+e);}};

		this.addToFeeds=function(){try{
			WM.feedManager.newFeed({id:this.fromID, title:this.fromName});
			WM.feedManager.save();
		}catch(e){log("WM.Post.addToFeeds: "+e);}};

		this.accept=function(mark){try{
			this.isAccepted=true;
			this.isFailed=false;
			this.isTimeout=false;
			this.isWorking=false;
			this.isCollect=false;
			if (mark) WM.setAsAccepted(null, 3,this);
		}catch(e){log("WM.Post.accept: "+e);}};

		this.fail=function(mark){try{
			this.isFailed=true;
			this.isAccepted=false;
			this.isTimeout=false;
			this.isWorking=false;
			this.isCollect=false;
			if (mark) WM.setAsFailed(null, -18,this);
		}catch(e){log("WM.Post.fail: "+e);}};

		this.timeout=function(){try{
			this.isTimeout=true;
			this.isAccepted=false;
			this.isFailed=false;
			this.isWorking=false;
			this.isCollect=false;
		}catch(e){log("WM.Post.timeout: "+e);}};

		this.remove=function(){try{
			var node=(this.node||$("post_"+this.id));
			if (node && node.parentNode) remove(node);
			this.node=null;
			
			//turn this post into a ghost so we can keep its data
			//for linked objects, but not process it in reid or redraw
			this.isGhost=true;
			
			//delete WM.posts[this.id];
		}catch(e){log("WM.Post.remove: "+e);}};

		this.addToolBox=function(){try{
			var post=this;
			if (!WM.opts.showtoolbox) return;
			var toolNode = post.toolboxNode;
			if (toolNode) toolNode.appendChild(
				createElement("div",{},[
					createElement("div",{onclick:function(){post.forceOpen();},title:"Open Post",className:"littleButton oddBlue"+((!WM.opts.showopen)?" hidden":"")},[
						createElement("img",{className:"resourceIcon action"+WM.opts.littleButtonSize})]),
					createElement("div",{onclick:function(){post.cancelProcess();},title:"Cancel Process or AutoLike",className:"littleButton oddBlue"+((!WM.opts.showcancelprocess)?" hidden":"")},[
						createElement("img",{className:"resourceIcon cancelProcess"+WM.opts.littleButtonSize})]),
					createElement("div",{onclick:function(){post.refreshProcess();},title:"Restart Process or AutoLike",className:"littleButton oddBlue"+((!WM.opts.showrestartprocess)?" hidden":"")},[
						createElement("img",{className:"resourceIcon refreshProcess"+WM.opts.littleButtonSize})]),
					createElement("div",{onclick:function(){WM.pauseByType(post.app, post.which);},title:"Pause all bonuses of this type",className:"littleButton oddOrange"+((!WM.opts.showpausetype)?" hidden":"")},[
						createElement("img",{className:"resourceIcon stop"+WM.opts.littleButtonSize})]),
					createElement("div",{onclick:function(){WM.unPauseByType(post.app, post.which);},title:"Unpause all bonuses of this type",className:"littleButton oddGreen"+((!WM.opts.showunpausetype)?" hidden":"")},[
						createElement("img",{className:"resourceIcon playRight"+WM.opts.littleButtonSize})]),
					createElement("div",{onclick:function(){window.open(post.permalink,"_blank");},title:"Show Post Source",className:"littleButton oddBlue"+((!WM.opts.showpostsrc)?" hidden":"")},[
						createElement("img",{className:"resourceIcon openInNewWindow"+WM.opts.littleButtonSize})]),
					createElement("div",{onclick:function(){post.remove();},title:"Clean",className:"littleButton oddOrange"+((!WM.opts.showclean)?" hidden":"")},[
						createElement("img",{className:"resourceIcon trash"+WM.opts.littleButtonSize})]),
					createElement("div",{onclick:function(){post.togglePin();},title:"Pin",className:"littleButton"+((!WM.opts.showpin)?" hidden":"")},[
						post.pinImageNode=createElement("img",{className:"resourceIcon "+(post.isPinned?"pinned":"pin")+WM.opts.littleButtonSize})]),
					createElement("div",{onclick:function(){post.moveToBottom();},title:"Move to Bottom",className:"littleButton oddOrange"+((!WM.opts.showmovebottom)?" hidden":"")},[
						createElement("img",{className:"resourceIcon moveBottomLeft"+WM.opts.littleButtonSize})]),
					createElement("div",{onclick:function(){post.moveToTop();},title:"Move to Top",className:"littleButton oddGreen"+((!WM.opts.showmovetop)?" hidden":"")},[
						createElement("img",{className:"resourceIcon moveTopLeft"+WM.opts.littleButtonSize})]),
					createElement("div",{onclick:function(){post.reID();},title:"ReID Post",className:"littleButton"+((!WM.opts.showreid)?" hidden":"")},[
						createElement("img",{className:"resourceIcon identify"+WM.opts.littleButtonSize})]),
					createElement("div",{onclick:function(){post.addToFeeds();},title:"Add To Feeds",className:"littleButton"+((!WM.opts.showaddfeed)?" hidden":"")},[
						createElement("img",{className:"resourceIcon addFeed"+WM.opts.littleButtonSize})]),
					post.likeButtonNode=createElement("div",{onclick:function(){post.like();},title:"Like Post",className:"likeButton littleButton oddBlue"+(post.isLiked?" hidden":"")+((!WM.opts.showlike)?" hidden":"")},[
						createElement("img",{className:"resourceIcon like"+WM.opts.littleButtonSize})]),
					
					createElement("div",{onclick:function(){post.comment();},title:"Auto Comment",className:"littleButton oddBlue"+((!WM.opts.showautocomment)?" hidden":"")},[
						createElement("img",{className:"resourceIcon comment"+WM.opts.littleButtonSize})]),
					
					createElement("div",{onclick:function(){post.fail(true);},title:"Mark as Failed",className:"littleButton oddOrange"+((!WM.opts.showmarkfailed)?" hidden":"")},[
						createElement("img",{className:"resourceIcon minus"+WM.opts.littleButtonSize})]),
					createElement("div",{onclick:function(){post.accept(true);},title:"Mark as Accepted",className:"littleButton oddGreen"+((!WM.opts.showmarkaccepted)?" hidden":"")},[
						createElement("img",{className:"resourceIcon plus"+WM.opts.littleButtonSize})]),
					createElement("div",{onclick:function(){WM.rulesManager.ruleFromPost(post);},title:"Create Rule From Post",className:"littleButton oddBlue"+((!WM.opts.showmakerule)?" hidden":"")},[
						createElement("img",{className:"resourceIcon gotoLibrary"+WM.opts.littleButtonSize})]),
					createElement("div",{onclick:function(){promptText(self.originalData,true);},title:"Show Original Data",className:"littleButton"+((!WM.opts.showoriginaldata)?" hidden":"")},[
						createElement("img",{className:"resourceIcon object"+WM.opts.littleButtonSize})]),
				])
			);
		}catch(e){log("WM.Post.addToolBox: "+e);}};

		/*
		this.__defineGetter__("linkText", function(){try{
			if (this.actions.length >=3) return this.actions.last().name||"";
			else return "";
		}catch(e){log("WM.Post.linkText: "+e);}});

		this.__defineGetter__("linkHref", function(){try{
			return this.link||((this.actions.length>=3)?(this.actions.last().link||""):"")||"";

			if (this.actions.length >=3) return this.actions.last().link||"";
			else return this.link||"";
		}catch(e){log("WM.Post.linkHref: "+e);}});
		*/

		/*this.actionLink=function(action){try{
			//for (var a=0,act;(act=this.actions[a]);a++) if (act.name.toLowerCase()==action.toLowerCase()) {return act.link; break;}
			return this.linkHref;
		}catch(e){log("WM.Post.actionLink: "+e);}};*/

		this.__defineGetter__("body", function(){try{
			return (this.title||"")+" "+(this.caption||"")+" "+(this.description||"");
		}catch(e){log("WM.Post.body: "+e);}});

		this.__defineGetter__("either", function(){try{
			return this.linkText+" "+this.body;
		}catch(e){log("WM.Post.either: "+e);}});

		/*this.__defineGetter__("date", function(){try{
			return this["created_time"];
		}catch(e){log("WM.Post.date: "+e);}});*/

		this.checkStale=function(timeOverride) {try{
			if (exists(timeOverride) && timeOverride==0) return false;
			var now = timeStamp();
			var pubTime = this.date*1000; //(this.date.length < 10)?this.date+"000":this.date;
			//var pubTime = this.date+"000";
			var aDay = (1000 * 60 * 60 * 24);
			return (now-pubTime)>(timeOverride||aDay);
		}catch(e){log("WM.Post.checkStale: "+e);}};

		//req must equal "id" or "name"
		/*this.getTargets=function(req){try{
			req = req||"id";
			var ret = [];
			if (exists(this.to)) {
				for (var i=0,target; (target=this.to.data[i]);i++) ret.push(target[req]);
			}
			return ret;
		}catch(e){log("WM.Post.getTargets: "+e);}};*/

		//ret must equal "id" or "message" or "name" or "count"
		this.getComments=function(req){try{
			var ret = [];
			if (exists(this.comments)) if (this.comments.count) {
				switch(req){
					case "message": for (var i=0,comment; (comment=this.comments.data[i]);i++) ret.push(comment[req]); break;
					case "id":case "name": for (var i=0,comment; (comment=this.comments.data[i]);i++) ret.push(comment.from[req]); break;
					case "count": return this.comments.count; break;
				}
			}
			return ret;
		}catch(e){log("WM.Post.getComments: "+e);}};

		//ret must equal "id" or "name" or "count"
		this.getLikes=function(req){try{
			req = req||"id";
			var ret = [];
			if (exists(this.likes)) if (this.likes.count) {
				switch(req){
					case "id":case "name": for (var i=0,like; (like=this.likes.data[i]); i++) ret.push(like[req]); break;
					case "count": return this.likes.count; break;
				}
			}
			return ret;
		}catch(e){log("WM.Post.getLikes: "+e);}};

		this.moveToTop = function(){try{
			if (this.node||null) this.node.parentNode.insertBefore(this.node,this.node.parentNode.childNodes[0]);
		}catch(e){log("WM.Post.moveToTop: "+e);}};

		this.moveToBottom = function(){try{
			if (this.node||null) this.node.parentNode.appendChild(this.node);
		}catch(e){log("WM.Post.moveToBottom: "+e);}};

		this.setPriority = function(value){try{
			this.priority=value;
			remove(this.node);
			this.draw();
		}catch(e){log("WM.Post.setPriority: "+e);}};		
		
		this.reID = function(params){try{
			params=params||{};
						
			//reidentify
			var oldW=this.which;
			if (this.identify({reid:true})) {
				WM.rulesManager.doEvent("onIdentify",this);
			}
			
			//sort me into proper location
			WM.sortPosts();

			//redraw||reorder
			if (!this.isGhost) {
				this.draw(true,true);
			}
		}catch(e){log("WM.Post.reID: "+e);}};

		//return the next visible sibling post
		//now checks for group and visibility
		this.nextSibling = function(){try{
			//determine if there is an app filter
			var filter=(WM.quickOpts.filterApp||"All");
			
			//determine display grouping
			var groupBy=WM.quickOpts.groupBy;
			var group=(groupBy)?WM.newGroup({by:this[groupBy]}):null;
			
			//get visible posts
			var visiblePosts=(filter=="All")?WM.posts:matchByParam(WM.posts,"appID",filter);
			if (groupBy) visiblePosts=matchByParam(visiblePosts,groupBy,this[groupBy]);
			
			//search for the current post
			var found=false, sibling=null;
			for (var p in visiblePosts) {
				if (found && visiblePosts[p].node) {
					if ((!groupBy && visiblePosts[p].node.parentNode==WM.console.feedNode) 
					|| (groupBy && visiblePosts[p].node.parentNode==group)){
						sibling=visiblePosts[p]; 
						break
					}
				} else if (visiblePosts[p]==this) found=true;
			}

			//return what is found
			return sibling;
			//warning: returns null if this is the last visible post
		}catch(e){log("WM.Post.nextSibling: "+e);}};

		//return the previous visible sibling post
		//now checks for group and visibility
		this.previousSibling = function(){try{
			//determine if there is an app filter
			var filter=(WM.quickOpts.filterApp||"All");
			
			//determine display grouping
			var groupBy=WM.quickOpts.groupBy;
			var group=(groupBy)?WM.newGroup({by:this[groupBy]}):null;

			//get visible posts
			var visiblePosts=(filter=="All")?WM.posts:matchByParam(WM.posts,"appID",filter);
			if (groupBy) visiblePosts=matchByParam(visiblePosts,groupBy,this[groupBy]);
			
			//search for the current post
			var sibling=null;
			for (var p in visiblePosts) {
				if (visiblePosts[p]==this) break;
				else if (visiblePosts[p].node) {
					if ((!groupBy && visiblePosts[p].node.parentNode==WM.console.feedNode)
					|| (groupBy && visiblePosts.node.parentNode==group)){
						sibling=visiblePosts[p];
					}
				}
			}
			
			//return what is found
			return sibling;
			//warning: returns null if this is the first visible post
		}catch(e){log("WM.Post.previousSibling: "+e);}};
		


		
		//get the friend object related with this post (from the friend tracker)
		this.__defineGetter__("relatedFriend",function(){try{
			return WM.friendTracker.friends[this.fromID]||null;
		}catch(e){log("WM.Post.relatedFriend: "+e);}});
		
		this.__defineGetter__("friendAcceptedCount",function(){try{
			return this.relatedFriend.acceptCount;
		}catch(e){log("WM.Post.friendAcceptedCount: "+e);}});

		this.__defineGetter__("friendFailedCount",function(){try{
			return this.relatedFriend.failCount;
		}catch(e){log("WM.Post.friendFailedCount: "+e);}});

		//a little cleanup
		params = null;
	}catch(e){log("WM.Post.init: "+e);}};

	
	
//***************************************************************************************************************************************
//***** Main Program
//***************************************************************************************************************************************

	
	//*****short text versions for WM.config menu options*****

	var checkBox=function(l,d,c,n){return ({type:"checkbox",label:l,"default":d||false,kids:c,newitem:n});}
	var hidden=function(l,d,c){return ({type:"hidden",label:l,"default":d,kids:c});}
	var optionBlock=function(l,c,hideSelectAll,n){hideSelectAll=hideSelectAll||false;return ({type:"optionblock",label:l,kids:c,hideSelectAll:hideSelectAll,newitem:n});}
	var separator=function(l,s,c,hideSelectAll){hideSelectAll=hideSelectAll||false; return ({type:"separator",label:l,section:s,kids:c}); }
	var section=function(l,c){return ({type:"section",label:l,kids:c});}
	var tabSection=function(l,c){return ({type:"tab",label:l,kids:c});}
	var inputBox=function(l,d,n){return ({type:"float",label:l,"default":(d||0),newitem:n});}
	var textArea=function(l,d,n){return ({type:"textarea",label:l,"default":(d||0),newitem:n});}
	var colorBox=function(l,d,n){return ({type:"colorbox",label:l,"default":(d||"black"),newitem:n});}
	var button=function(l,s){return ({type:"button",label:l,script:s});}
	var anchor=function(l,u,t){return ({type:"link",label:l,href:u,title:(t||'')});}


//***************************************************************************************************************************************
//***** Immediate
//***************************************************************************************************************************************

	log("Script: WM initialized",{level:0});

	// section for reclaiming memory and stopping memory leaks
	var newIntv=null; //refresh interval
	var oldIntv=null; //refresh interval
	var procIntv=null; //process interval
	var cleanIntv=null; //post removal interval
	var hbIntv=null; //global heartbeat interval

	var olderLimit=day; //default 1 day

	var cleanup=function() {try{
		//destroy intervals
		if (newIntv) window.clearInterval(newIntv);
		if (oldIntv) window.clearInterval(oldIntv);
		if (procIntv) window.clearInterval(procIntv);
		if (cleanIntv) window.clearInterval(cleanIntv);
		if (hbIntv) window.clearInterval(hbIntv);
		oldIntv = newIntv = procIntv = cleanIntv = hbIntv = null;

		//close the sidekick tabs
		WM.collector.closeAll();

		//remove this event listener
		window.removeEventListener("beforeunload", cleanup, false);
		window.removeEventListener("message", WM.receiveSidekickMessage, false);
		window.removeEventListener("resize", WM.onWindowResize, false);

		//clean up memory
		WallManager=null;
		Graph=null;
		jsForms=null;
		olderLimit=null;
		opts=null; quickOpts=null;
		
	}catch(e){log("cleanup: "+e);}}
	window.addEventListener("beforeunload", cleanup, false);
	window.addEventListener("resize", WM.onWindowResize, false);
	
	//start it up
	WM.run();
	
})(); // anonymous function wrapper end

/* recent changes
	3.1.1
	*fixed the update script button to point to the standard branch
	*posts are once again able to fetch source user names with the post
	*reinstated post.showOriginalData so developers can see the return data from FQL and how it differs from GraphAPI returns
	
	3.1.2
	*fixed the button on feed manager where you can fetch for a specific friend feed manually
	*added option to fetch all apps for a specific feed in a single request, default is to fetch for each app with a separate request
	*fixed drawing order bug caused by graph update
	*temporarily removed checking for feed already having reached older limit, feed will now draw until facebook has no posts to show if you tell it to 
	**hide stale posts will still make it so you can't see those posts if you have it enabled
	*fixed friend tracker last known post date
	
	3.1.3
	*when action_links is null from facebook, WM now uses attachment.href for the link, which should match, but is not a guarantee.
	*when action_links is null from facebook, WM now uses "" as the link text. Neither attachment or app_data appears to have a duplicate of the link text.
	*added error checking for if the return data for a post cannot be transformed into a useful post object. This could happen if some of the data WM requires to function is not available, or is mangled for some reason.
	*added option to have WM filter posts by app instead of asking FB for posts by app. This will hopefully remove a lot of that empty data set issue, but you will occasionally get return data without any useful posts in it.
	
	3.1.4
	*reinstated olderLimitReached and any function or rules manager event having to do with that
	*removed some console.log debuggery
	*fixed issue with disabled feeds still being processed when fetching multiple apps at once (with those new options)
	
	3.1.4.1
	*fixed alternative spotting of link url and link text associated with app posts (when action_links data is null)
	
	3.1.6
	*added rule manager options: friendAcceptCount and friendFailedCount
*/