WM Graph API Interface (Beta Branch)

Creates a low-access, read-only interface to the FB Graph API.

This script should not be not be installed directly. It is a library for other scripts to include with the meta directive // @require https://update.greasyfork.org/scripts/411/1288/WM%20Graph%20API%20Interface%20%28Beta%20Branch%29.js

// ==UserScript==
// @name          	WM Graph API Interface (Beta Branch)
// @namespace           graphtemp
// @description	Creates a low-access, read-only interface to the FB Graph API.
// @require https://greasyfork.org/scripts/407-wm-common-library/code/WM%20Common%20Library.user.js?version=1284
// @license		http://creativecommons.org/licenses/by-nc-nd/3.0/us/
// @version        	3.1.3
// @copyright      	Charlie Ewing except where noted
// ==/UserScript==

//this script requires some functions in the WM Common Library
//this script needs access to a pre-defined JSON object

var workOffline=false;

(function(){
	this.Graph = {
		posts: {}, //location to store adjusted post data
		authRequestOut: false, //dont ask for token while asking for token
		authToken: (isChrome||getOpt("disableSaveAuthToken"))?null:getOpt("lastAuthToken"), //use stored fb token
		userID: null,
		userAlias: null,
		userName: null,
		
		fetchTimeout: 30,

		requests: [],
		
		likePost: function(postID,params){try{
			//https://graph.facebook.com/POST_ID/likes?access_token=		
			var req; req=GM_xmlhttpRequest({
				method: "POST",
				url: "https://graph.facebook.com/"+postID+"/likes?access_token="+Graph.authToken,
				timeout: Graph.fetchTimeout*1000,
				onload: function(response) {try{
					if (response.responseText=="true") {
						if (params.callback) params.callback(params.post);
					} else {
						log(response.responseText);
					}
				}catch(e){log("Graph.likePost.onload: "+e);}}, 
				onerror: function(response) {try{
				}catch(e){log("Graph.likePost.onerror: "+e);}},
				onabort: function(response) {try{
				}catch(e){log("Graph.likePost.onabort: "+e);}},
				ontimeout: function(response) {try{
				}catch(e){log("Graph.likePost.ontimeout: "+e);}}
			});
		}catch(e){log("Graph.likePost: "+e);}},

		unlikePost: function(postID){try{
			//https://graph.facebook.com/POST_ID/likes?access_token=		
			var req; req=GM_xmlhttpRequest({
				method: "DELETE",
				url: "https://graph.facebook.com/"+postID+"/likes?access_token="+Graph.authToken,
				timeout: Graph.fetchTimeout*1000,
				onload: function(response) {try{
				}catch(e){log("Graph.unlikePost.onload: "+e);}}, 
				onerror: function(response) {try{
				}catch(e){log("Graph.unlikePost.onerror: "+e);}},
				onabort: function(response) {try{
				}catch(e){log("Graph.unlikePost.onabort: "+e);}},
				ontimeout: function(response) {try{
				}catch(e){log("Graph.unlikePost.ontimeout: "+e);}}
			});
		}catch(e){log("Graph.unlikePost: "+e);}},

		commentPost: function(postID,comment){try{
			//https://graph.facebook.com/POST_ID/comments?message=&access_token=	
			var req; req=GM_xmlhttpRequest({
				method: "POST",
				url: "https://graph.facebook.com/"+postID+"/comments?access_token="+Graph.authToken+"&message="+comment,
				timeout: Graph.fetchTimeout*1000,
				onload: function(response) {try{
					if (response.responseText=="true") {
						//comment successful
					} else {
						log(response.responseText);
					}
				}catch(e){log("Graph.commentPost.onload: "+e);}}, 
				onerror: function(response) {try{
				}catch(e){log("Graph.commentPost.onerror: "+e);}},
				onabort: function(response) {try{
				}catch(e){log("Graph.commentPost.onabort: "+e);}},
				ontimeout: function(response) {try{
				}catch(e){log("Graph.commentPost.ontimeout: "+e);}}
			});
		}catch(e){log("Graph.commentPost: "+e);}},
		
		requestAuthCodeB: function(callback){try{
			log("Graph.requestAuthCodeB()");
			if (Graph.authRequestOut) return {requestAlreadyOut:true}; //dont ask again while asking
			Graph.authRequestOut = true;
			var req; req=GM_xmlhttpRequest({
				method: "GET",
				url: "http://developers.facebook.com/tools/explorer",
				timeout: Graph.fetchTimeout*1000,
				onload: function(response) {try{
					var test=response.responseText;
					var auth=test.longestQuoteWithin();
					if (auth!="") {
						Graph.authToken = auth;
						log("Graph.requestAuthCodeB: got token");
						setOpt("lastAuthToken",auth);
					} else {
						log("Graph.requestAuthCodeB: "+response.responseText,{level:3});					
					}					
					Graph.authRequestOut=false;
					if (callback) setTimeout(callback,0);
					if(req)req=null;
				}catch(e){log("Graph.requestAuthCodeB.onload: "+e);}}, 
				onerror: function(response) {try{
					Graph.authToken="";
					Graph.authRequestOut=false;
					log("Graph.requestAuthCodeB: error:"+response.responseText+"\n Trying again in 30 seconds.",{level:3});
					setTimeout(function(){Graph.requestAuthCodeB(callback);},30000);
					if(req)req=null;
				}catch(e){log("Graph.requestAuthCodeB.onerror: "+e);}},
				onabort: function(response) {try{
					Graph.authToken="";
					Graph.authRequestOut=false;
					log("Graph.requestAuthCodeB: Request aborted, trying again in 30 seconds",{level:3});
					if(req)req=null;
					setTimeout(function(){Graph.requestAuthCodeB(callback);},30000);
				}catch(e){log("Graph.requestAuthCodeB.onabort: "+e);}},
				ontimeout: function(response) {try{
					Graph.authToken="";
					Graph.authRequestOut=false;
					log("Graph.requestAuthCodeB: Request timeout, trying again in 30 seconds",{level:3});
					if(req)req=null;
					setTimeout(function(){Graph.requestAuthCodeB(callback);},30000);
				}catch(e){log("Graph.requestAuthCodeB.ontimeout: "+e);}}
			});
			
		}catch(e){log("Graph.requestAuthCodeB: "+e);}},
		
		requestAuthCode: function(callback){try{
			log("Graph.requestAuthCode()");
			if (Graph.authRequestOut) return {requestAlreadyOut:true}; //dont ask again while asking
			Graph.authRequestOut = true;
			var req; req=GM_xmlhttpRequest({
				method: "GET",
				url: "http://developers.facebook.com/docs/reference/api/examples/",
				timeout: Graph.fetchTimeout*1000,
				onload: function(response) {try{
					var test=response.responseText;
					var searchString='<a href="https://graph.facebook.com/me/home?access_token=';
					var auth = test.indexOf(searchString),authEnd;
					if (auth!=-1) {
						authEnd = test.indexOf('">',auth);
						var authCode = (test.substring(auth+(searchString.length), authEnd));
						Graph.authToken = authCode;
						setOpt("lastAuthToken",authCode);
						log("Graph.requestAuthCode: got token");
					} else {
						log("Graph.requestAuthCode: "+response.responseText,{level:3});
					}
					Graph.authRequestOut=false;
					if (callback) setTimeout(callback,0);
					if(req)req=null;
				}catch(e){log("Graph.requestAuthCode.onload: "+e);}}, 
				onerror: function(response) {try{
					Graph.authToken="";
					Graph.authRequestOut=false;
					log("Graph.requestAuthCode: error:"+response.responseText+"\n Trying again in 30 seconds.",{level:3});
					setTimeout(function(){Graph.requestAuthCode(callback);},30000);
					if(req)req=null;
				}catch(e){log("Graph.requestAuthCode.onerror: "+e);}},
				onabort: function(response) {try{
					Graph.authToken="";
					Graph.authRequestOut=false;
					log("Graph.requestAuthCode: Request aborted, trying again in 30 seconds",{level:3});
					if(req)req=null;
					setTimeout(function(){Graph.requestAuthCode(callback);},30000);
				}catch(e){log("Graph.requestAuthCode.onabort: "+e);}},
				ontimeout: function(response) {try{
					Graph.authToken="";
					Graph.authRequestOut=false;
					log("Graph.requestAuthCode: Request timeout, trying again in 30 seconds",{level:3});
					if(req)req=null;
					setTimeout(function(){Graph.requestAuthCode(callback);},30000);
				}catch(e){log("Graph.requestAuthCode.ontimeout: "+e);}}
			});
			
		}catch(e){log("Graph.requestAuthCode: "+e);}},

		fetchUser: function(params){try{
			log("Graph.fetchUser()");
			params=params || {};
			
			if (!Graph.authToken) {
				log("Graph.fetchUser: no authToken, get one");
				params["retries_noToken"]=(params["retries_noToken"])?params["retries_noToken"]+1:1; //count retries
				if (params["retries_noToken"]<3) {
					Graph.requestAuthCodeB(function(){Graph.fetchUser(params);} ); 
				} else {
					log("Graph.fetchUser: cannot get new fb auth token",{level:3});
					return {getAuthTokenFailed:true}
				}
				return;
			}
			var URL="https://graph.facebook.com/me?access_token="+Graph.authToken;
			var req; req=GM_xmlhttpRequest({
				method: "GET",
				url: URL,
				timeout: Graph.fetchTimeout*1000,
				onload: function(response) {try{
					if (response){
						//convert to JSON
					   try{
						var data = JSON.parse(response.responseText);
				
						if (data["id"]||null){
							//expected data exists
							Graph.userID=data["id"||null];
							Graph.userAlias=(data["username"]||null);
							Graph.userName=(data["name"]||null);

							if (params["callback"]) {
								var fx=params["callback"];
								delete params["callback"];
								setTimeout(fx,0);
							}
							
						} else if (data["error"]||null) {
							var emsg=data.error.message||null;
							//check for session expired
							if (emsg.find("Session has expired")||emsg.find("session is invalid")){
								//session expired or logged out, get a new token
								Graph.authToken="";
								params["retries_expToken"]=(params["retries_expToken"])?params["retries_expToken"]+1:1; //count retries
								if (params["retries_expToken"]<3) {
									Graph.requestAuthCodeB(function(){Graph.fetchUser(params);} ); 
								} else log("Graph.fetchUser: cannot refresh expired fb auth token",{level:3});
							} else if (emsg) log("Graph.fetchUser: "+emsg,{level:3});

						} else log("Graph.fetchUser: response was unrecognized",{level:3});
					    } catch (e){log("Graph.fetchUser: response error: "+e+": "+response);}

					} else log("Graph.fetchUser: response was empty",{level:3});
					if(req)req=null;
				}catch(e){log("Graph.fetchUser.onload: "+e);}}, 

				onabort: function(response) {try{
					log("Graph.fetchUser: Request aborted, trying again in 30 seconds.");
					setTimeout(function(){Graph.fetchUser(params);},30000);
					if(req)req=null;
				}catch(e){log("Graph.fetchUser.onabort: "+e);}},
				
				ontimeout: function(response) {try{
					log("Graph.fetchUser: Request timeout, trying again in 30 seconds.");
					setTimeout(function(){Graph.fetchUser(params);},30000);
					if(req)req=null;
				}catch(e){log("Graph.fetchUser.ontimeout: "+e);}},

				onerror: function(response) {try{
					if (response.responseText=="") {
						log(JSON.stringify(response));
						log("Graph.fetchUser: responseText was empty. Check to make sure your browser is online.", {level:5});
					}
					var data = JSON.parse(response.responseText);
					if (data) {if (data.error||null) {
						var emsg=data.error.message||null;
						//check for session expired
						if (emsg.find("Session has expired")||emsg.find("session is invalid")){
							//session expired or logged out, get a new token
							Graph.authToken="";
							params["retries_expToken"]=(params["retries_expToken"])?params["retries_expToken"]+1:1; //count retries
							if (params["retries_expToken"]<3) {
								Graph.requestAuthCodeB(function(){Graph.fetchUser(params);} ); 
							} else log("Graph.fetchUser: cannot refresh expired fb auth token",{level:3});
						} else if (emsg) log("Graph.fetchUser.onerror: "+emsg,{level:3});
					}} else {
						log("Graph.fetchUser.onerror: "+response+"\n Trying again in 30 seconds.");
						setTimeout(function(){Graph.fetchUser(params);},30000);
						if(req)req=null;
					}
				}catch(e){log("Graph.fetchUser.onerror: "+e);}}
			}); 
		}catch(e){log("Graph.fetchUser: "+e);}},

		matchRequest: function(params){try{
			for (var r in Graph.requests) {
				var req = Graph.requests[r];

				//match the feed
				if (JSON.stringify(req.friends) == JSON.stringify(params.friends)){ 
					//match the app filters
					if (JSON.stringify(req.apps) == JSON.stringify(params.apps)) { 
						//match direction of request
						if (req.direction==params.direction) { 
							return r;
						}
					}
				}
			}
			return -1;
		}catch(e){log("Graph.matchRequest: "+e);}},

		validatePost: function(params){try{
			var post=params.post;
			var callback=params.callback;
			var isOlder=params.next;

			//log("Graph.validatePost()",{level:1});

			//exclude non-app posts and posts with no action links
			//if (!exists(post.actions||null) || !exists(post.application)) return;

			//exclude posts with less than like and comment and which have no link
			//if (!(post.actions.length>=2) || !exists(post.link)) return;
			
			var postID=post["post_id"]||post["id"];

			//exclude posts already in our repository
			if (exists(Graph.posts[postID])) return;

			//store a reference to this post
			Graph.posts[postID]=1;

			//send the post back to the callback function here
			if (callback) setTimeout(function(){callback(post,isOlder);},0);
		}catch(e){log("Graph.validatePost: "+e);}},
		
		fetchPostsFQL_B: function(params){try{
			console.log(JSON.stringify(params));
		
			if (arguments.length==0) {
				log("Graph.fetchPostsFQL: no parameters passed");
				return;
			}
			
			/*
				direction: 0=until now | 1=forward from front edge | -1=backward from back edge
				apps = array of app id's to fetch posts for, error on no apps passed
				friends = array of friend id's to fetch posts for, default all friends
				limit = number to fetch
				timeouts = number of timeouts so far when performing retry looping
				targetEdge = unix time to continue fetching until
				currentEdge = current remembered edge of feed
				retries_noToken = number of times this function has called getAuthToken and failed
				callback = function to enact on each post
				edgeHandler = function to keep track of edges
			*/
			
			if (!(params.apps||null)) {
				log("Graph.fetchPostsFQL: no apps requested");
				return;
			}
		
			var bypassMatchRequest = (params.targetEdge||null)?true:false;

			//validate current auth token
			if (!Graph.authToken) {
				log("Graph.fetchPostsFQL: no authToken, get one");
				params["retries_noToken"]=(params["retries_noToken"])?params["retries_noToken"]+1:1; //count retries
				if (params["retries_noToken"]<3) {
					Graph.requestAuthCodeB(function(){Graph.fetchPostsFQL_B(params);} ); 
				} else {
					log("Graph.fetchPostsFQL: cannot get new fb auth token",{level:3});
					return {getAuthTokenFailed:true};
				}
				return;
			}

			//check if there is a request already out with this feed id and matches the direction
			if (!bypassMatchRequest) {
				var r=Graph.matchRequest(params);
				if (r!=-1){
					log("Graph.fetchPostsFQL: a request is already out for posts in that direction and has not returned",{level:3});
					return {requestAlreadyOut:true};
				}
			}
									
			//compile feed request strings			
			var URL_prefix="https://graph.facebook.com/fql?access_token={0}&q=";
			var URL="{\"query1\":\"SELECT post_id,target_id,message,app_id,action_links,created_time,attachment,app_data,like_info,source_id FROM stream WHERE source_id IN ({3}){1}{2} ORDER BY created_time DESC{4}\",\"query2\":\"SELECT uid,name FROM user WHERE uid IN (SELECT source_id FROM #query1)\"}";
			URL_prefix=URL_prefix.replace("{0}",Graph.authToken);
			
			//specialize call for specific friend post requests
			if (params.friends||null) {
				URL=URL.replace("{3}",params.friends.join(","));
			} else {
				URL=URL.replace("{3}","SELECT uid2 FROM friend WHERE uid1=me() LIMIT 5000");
			}
			
			//get older posts
			//verify that the feed "until" time does not violate olderLimit set by user
			if (params.direction<0){
				URL=URL.replace("{2}"," AND created_time < "+params.currentEdge);
				
			
			//get newer posts
			} else if (params.direction>0){
				URL=URL.replace("{2}"," AND created_time > "+params.currentEdge);
				
			//fetch at current time
			} else {
				URL=URL.replace("{2}","");
			}

			//filters by apps requested
			//unless no apps were passed or we specified no app filtering
			if ((params.apps||null) && !(params.noAppFiltering||null)){
				URL=URL.replace("{1}"," AND app_id IN ("+params.apps.join(",")+")");
			} else {
				//no app filtering, let WM do this internally
				URL=URL.replace("{1}","");
			}
						
			//add the user defined fetchQty
			URL=URL.replace("{4}","+LIMIT+"+(params.limit||25));
			
			//encode the url
			URL=URL_prefix+encodeURI(URL).replace(/\:/,"%3A").replace(/\#/,"%23");

			log("Graph.fetchPostsFQL: processing feed <a target='_blank' href='"+URL+"'>"+URL+"</a>");
			
			//remember this request
			Graph.requests.push(mergeJSON(params));

			//make the request
			var req; req=GM_xmlhttpRequest({
				method: "GET",
				url: URL,
				timeout: Graph.fetchTimeout*1000,
				onload: function(response) {try{
					//show dev tools
					if (opts && debug && !isChrome) if (opts.devDebugGraphData) {
						var pkg=debug.print("Graph.fetchPostsFQL.onload.devDebugGraphData: ");
						pkg.msg.appendChild(createElement("button",{type:"button",onclick:function(){
								promptText(response.responseText);
							}},[
							createElement("img",{src:"http://i1181.photobucket.com/albums/x430/merricksdad/array.png",title:"Show Data",style:"width:16px;height:16px; vertical-align:bottom;"})
						]));
					}
					
					//remove the memory that a request is out
					var r = Graph.matchRequest(params);
					if (r!=-1) Graph.requests.remove(r);

					if (response){
					    try{
						//convert to JSON
						var data = JSON.parse(response.responseText);
												
						//manage the return object
						if (exists(data.data)) {							
							//there should be two return queries under data
							//zip the two together by matching the name to the source_id in the post
							var realData = data.data[0].fql_result_set;
							var nameData = data.data[1].fql_result_set;
							var uidKeys = {};
							
							for (var n=0,l=nameData.length;n<l;n++){
								uidKeys[nameData[n].uid.toString()]=nameData[n].name;
							}

							for (var p=0,l=realData.length;p<l;p++){
								realData[p].fromName = (uidKeys[realData[p].source_id.toString()]||"undefined");
							}
							
							data.data=realData;
	
							//store posts
							if (data.data.length) log("Graph.fetchPostsFQL.onLoad: "+data.data.length+" posts received. Validating data...");
							else log("Graph.fetchPostsFQL.onLoad: facebook returned an empty data set.");
							
							//no paging exists in the FQL system, we make our own
							var gotMoreToDo=false;
							var lastPullOldestPost=null;
							var lastPullNewestPost=null;
							if (data.data.length) {
								lastPullOldestPost=data.data.last().created_time;
								lastPullNewestPost=data.data[0].created_time;
							}
							
							if ((params.targetEdge||null) && (data.data.length)) {
								gotMoreToDo = (params.direction<0)?
									(lastPullOldestPost > params.targetEdge): //keep fetching older
									(lastPullNewestPost < params.targetEdge); //keep fetching newer
							}
							
							
							//read them in backward
							if (data.data.length) for (var i=data.data.length-1;i>=0;i--) {
								var post=data.data[i];

								//exclude posts already in our repository
								if (Graph.posts[post["post_id"]]!=1) {

									//store a reference to this post
									Graph.posts[post["post_id"]]=1;

									//send the post back to the callback function here
									params.callback(post);
								}
							}
							
							//process the edge handler for any request that returned posts
							var edgeMsg = {friends:params.friends,apps:params.apps,edge:{}};
							if (params.direction>=0) edgeMsg.edge.newer=lastPullNewestPost;
							if (params.direction<=0) edgeMsg.edge.older=lastPullOldestPost;
							if (data.data.length) if (params.edgeHandler||null) params.edgeHandler(edgeMsg);
							
							//go back and do another request if we have specified we have more to do
							//this is for use with fetchHours and similar functions
							if (gotMoreToDo) {
								log("Graph.fetchPostsFQL.onload: was not able to get enough in one return, going back for more...");
								var newParams = mergeJSON(params);
								newParams.currentEdge=(params.direction<0)?lastPullOldestPost:lastPullNewestPost;
								Graph.fetchPosts(newParams);
							}
							


						} else if (data.error||null) {
							//check for session expired
							if ((data.error.message||"").find("Session has expired")){
								//session expired, get a new token
								Graph.authToken="";
								params["retries_expToken"]=(params["retries_expToken"])?params["retries_expToken"]+1:1; //count retries
								if (params["retries_expToken"]<3) {
									Graph.requestAuthCodeB(function(){Graph.fetchPosts(params);} ); 
								} else log("Graph.fetchPostsFQL: cannot refresh expired fb auth token",{level:3});
							}
							else if (data.error.message||null) log("Graph.fetchPosts: "+data.error.message,{level:3});

						} else 	log("Graph.fetchPostsFQL: response was unrecognized",{level:3});
						data=null;
				    	    } catch (e){log("Graph.fetchPostsFQL: response error: "+e+": "+response);}	
					} else log("Graph.fetchPostsFQL: response was empty",{level:3});
					if(req)req=null;
				}catch(e){log("Graph.fetchPostsFQL.onload: "+e);}},

				onabort: function(response) {try{
					//remove the memory that a request is out
					var r = Graph.matchRequest(params);
					if (r!=-1) Graph.requests.remove(r);
					log("Graph.fetchPostsFQL: aborted: "+response.responseText);
					if(req)req=null;
				}catch(e){log("Graph.fetchPostsFQL.onabort: "+e);}},

				ontimeout: function(response) {try{
					//remove the memory that a request is out
					params.timeouts++;
					var r = Graph.matchRequest(params);
					if (r!=-1) Graph.requests.remove(r);
					log("Graph.fetchPostsFQL: timeout: retry="+(params.timeouts<3)+", "+response.responseText);
					if(req)req=null;
					
					if (params.timeouts<3) Graph.fetchPosts(params);
				}catch(e){log("Graph.fetchPostsFQL.ontimeout: "+e);}},

				onerror: function(response) {try{
					//remove the memory that a request is out
					var r = Graph.matchRequest(params);
					if (r!=-1) Graph.requests.remove(r);
					log("Graph.fetchPostsFQL: error: "+response.responseText);
					if(req)req=null;
				}catch(e){log("Graph.fetchPostsFQL.onerror: "+e);}}
			});		
		}catch(e){log("Graph.fetchPostsFQL_B: "+e);}},
		
		fetchPostsFQL: function(params){try{
			log("Graph.fetchPostsFQL("+((params.newer)?"newer":(params.older)?"older":"")+")",{level:1});
			params=params || {};
			params.timeouts=params.timeouts||0;			
			var bypassMatchRequest = (params.range||null)?true:false;
			
			//remember the target position if this is a ranged search
			//we'll pass targetrange back to this function later if we need to fetch more
			if (params.range||null){
				if (params.range.oldedge||null) params.targetedge = params.range.oldedge;
			}

			//validate auth token
			if (!Graph.authToken) {
				log("Graph.fetchPostsFQL: no authToken, get one");
				params["retries_noToken"]=(params["retries_noToken"])?params["retries_noToken"]+1:1; //count retries
				if (params["retries_noToken"]<3) {
					Graph.requestAuthCodeB(function(){Graph.fetchPostsFQL(params);} ); 
				} else {
					log("Graph.fetchPostsFQL: cannot get new fb auth token",{level:3});
					return {getAuthTokenFailed:true};
				}
				return;
			}

			//check if there is a request already out with this fb id and matches the direction
			var r=Graph.matchRequest(params);
			if (!bypassMatchRequest) if (r!=-1){
				if (Graph.requests[r].older==null && Graph.requests[r].newer==null) {
					log("Graph.fetchPostsFQL: the initial request for data has not been returned yet",{level:3});
					return {initRequestSlow:true};
				} else {
					log("Graph.fetchPostsFQL: a request is already out for posts in that direction and has not returned",{level:3});
					return {requestAlreadyOut:true};
				}
			}

			var feed=params.feed||null;
			var filter = (params.filter||"default");
			
			
			//create default filter instances when they do not exist
			if (params.groupApps||null) {
				//set our filter to our first app in the groupApps
				//this will be used below various times
				filter = "app_"+params.groupApps[0];
				
				if (feed||null) for (var a=0,l=params.groupApps.length;a<l;a++) {
					var filtName = "app_"+params.groupApps[a];
					if (!(feed.filters[filtName]||null)) feed.addFilter({id:filtName}); //create filter instance if needed					
				}
			} else {
				if (feed||null) if (!(feed.filters[filter]||null)) feed.addFilter({id:filter}); //create filter instance if needed
			}
			
			//compile feed request strings			
			var URL_prefix="https://graph.facebook.com/fql?access_token={0}&q=";
			var URL="{\"query1\":\"SELECT post_id,target_id,message,app_id,action_links,created_time,attachment,app_data,like_info,source_id FROM stream WHERE source_id IN ({3}){1}{2} ORDER BY created_time DESC{4}\",\"query2\":\"SELECT uid,name FROM user WHERE uid IN (SELECT source_id FROM #query1)\"}";
			URL_prefix=URL_prefix.replace("{0}",Graph.authToken);
			
			//specialize call for specific friend post requests
			if (params.specificFriend||null) {
				URL=URL.replace("{3}",feed.id);
			} else {
				URL=URL.replace("{3}","SELECT uid2 FROM friend WHERE uid1=me() LIMIT 5000");
			}
			
			//get older posts
			//verify that the feed "until" time does not violate olderLimit set by user
			if (params.older){
				var edge=(params.range||null)?params.range.oldedge:feed.filters[filter].oldedge; 
				if (edge||null){
					var limit=(params.limit||null); //this is not FB search limit keyword, this is a WM timelimit
					var timeNow=timeStamp();
					
					//no oldest post limit on range fetches
					if (params.range||null) limit=null;
										
					if (limit) {
						if ((timeNow-(edge*1000)) > limit) {
							log("Graph.fetchPosts("+params.feed.url+"): the user-set older limit of this feed has been reached",{level:2});
							return {olderLimitReached:true};
						}
					}
					URL=URL.replace("{2}"," AND created_time < "+edge);

				} else {
					log("Graph.fetchPostsFQL("+params.feed.url+"): The previous result did not return pagination. Restarting fetching from current time.");
					URL=URL.replace("{2}","");
				
				}
			
			//get newer posts
			} else if (params.newer){
				var edge=(params.range||null)?params.range.newedge:feed.filters[filter].newedge;
				if (exists(edge)) {
					URL=URL.replace("{2}"," AND created_time > "+edge);
				}
				
			//fetch at current time
			} else {
				URL=URL.replace("{2}","");
			}

			//filters come in the form of "app_123456789012"
			if (params.groupApps||null) {
				//fetch posts for multiple apps at once
				URL=URL.replace("{1}"," AND app_id IN ("+params.groupApps.join(",")+")");
			} else if (filter!=undefined && filter!=null){
				URL=URL.replace("{1}"," AND app_id IN ("+filter.split("app_")[1]+")");
			} else {
				//no filter, nothing passed
				//this should never happen
				URL=URL.replace("{1}","");
			}
			
			//add the user defined fetchQty
			URL=URL.replace("{4}","+LIMIT+"+(params.fetchQty||25));
			
			//encode the url
			URL=URL_prefix+encodeURI(URL).replace(/\:/,"%3A").replace(/\#/,"%23");

			log("Graph.fetchPostsFQL: processing feed <a target='_blank' href='"+URL+"'>"+URL+"</a>");
			//console.log(URL);
			//return;
			
			//remember this request
			Graph.requests.push({feed:params.feed, older:params.older, newer:params.newer, filter:filter, groupApps:params.groupApps, specificFriend:params.specificFriend});
			//console.log("request pushed");

			//return;
			var req; req=GM_xmlhttpRequest({
				method: "GET",
				url: URL,
				timeout: Graph.fetchTimeout*1000,
				onload: function(response) {try{
					//show dev tools
					if (opts && debug && !isChrome) if (opts.devDebugGraphData) {
						var pkg=debug.print("Graph.fetchPostsFQL.onload.devDebugGraphData: ");
						pkg.msg.appendChild(createElement("button",{type:"button",onclick:function(){
								//response.responseText.toClipboard();
								promptText(response.responseText);
							}},[
							createElement("img",{src:"http://i1181.photobucket.com/albums/x430/merricksdad/array.png",title:"Show Data",style:"width:16px;height:16px; vertical-align:bottom;"})
						]));
					}
					
					//remove the memory that a request is out
					var r = Graph.matchRequest(params);
					if (r!=-1) Graph.requests.remove(r);

					if (response){
					    try{
						//convert to JSON
						var data = JSON.parse(response.responseText);
												
						//manage the return object
						if (exists(data.data)) {
							//log("response contains data");
							
							//there should be two return queries under data
							//zip the two together by matching the name to the source_id in the post
							var realData = data.data[0].fql_result_set;
							var nameData = data.data[1].fql_result_set;
							var uidKeys = {};
							
							for (var n=0,l=nameData.length;n<l;n++){
								uidKeys[nameData[n].uid.toString()]=nameData[n].name;
							}

							for (var p=0,l=realData.length;p<l;p++){
								realData[p].fromName = (uidKeys[realData[p].source_id.toString()]||"undefined");
							}
							
							data.data=realData;
	
							//store posts
							if (data.data.length) log("Graph.fetchPostsFQL.onLoad: "+data.data.length+" posts received. Validating data...");
							else log("Graph.fetchPostsFQL.onLoad: facebook returned an empty data set.");
							
							//no paging exists in the FQL system, we make our own
							var gotMoreToDo=false;
							var lastPullOldestPost=null;
							var lastPullNewestPost=null;
							if (data.data.length) {
								lastPullOldestPost=data.data.last().created_time;
								lastPullNewestPost=data.data[0].created_time;
							}
							if ((params.targetedge||null) && (data.data.length)) {
								gotMoreToDo = (lastPullOldestPost > params.targetedge);
							}
							
							
							//read them in backward
							if (data.data.length) for (var i=data.data.length-1;i>=0;i--) {
								var post=data.data[i];

								Graph.validatePost({
									post:post,
									callback:params.callback||null,
									older:params.older,
									newer:params.newer
								});

							}
							
							//go back and do another request if we have specified we have more to do
							//this is for use with fetchHours and similar functions
							if (gotMoreToDo) {
								log("Graph.fetchPostsFQL.onload: was not able to get enough in one return, going back for more...");
								//clone the last set of params
								var newParams = mergeJSON(params);
								newParams.range={oldedge:0,newedge:0}; //new instance to prevent byRef errors
								//update the range settings
								newParams.range.newedge=lastPullOldestPost;
								newParams.range.oldedge=params.targetedge; //remember the original passed oldest data to target
								//make the next request
								Graph.fetchPosts(newParams);
							}
							
							//start cleanup
							if (params.callback) delete params.callback;


							//capture the next and prev urls, but dont overwrite current known time boundaries
							if (data.data.length){
								if (params.groupApps||null){
									//manage all filters at once from the group-fetch apps array
									for (var n=0,l=params.groupApps.length;n<l;n++){
										var filtName = "app_"+params.groupApps[n];
										
										
										if (!params.newer && !params.older) {
											feed.filters[filtName].newedge = lastPullNewestPost;
											feed.filters[filtName].oldedge = lastPullOldestPost;
										}
										if (params.newer) feed.filters[filtName].newedge = lastPullNewestPost;
										if (params.older) feed.filters[filtName].oldedge = lastPullOldestPost;
										
									}
								} else if (filter!=undefined && filter!=null) {
									//if the current request was a recent posts pull, set both edges
									if (!params.newer && !params.older) {
										feed.filters[filter].newedge = lastPullNewestPost;
										feed.filters[filter].oldedge = lastPullOldestPost;
									}

									//if the current request got newer posts, push the newer post edge
									if (params.newer) feed.filters[filter].newedge = lastPullNewestPost;
									
									//if the current request got older posts, push the older post edge
									if (params.older) feed.filters[filter].oldedge = lastPullOldestPost;
								}
							}

						} else if (data.error||null) {
							//check for session expired
							if ((data.error.message||"").find("Session has expired")){
								//session expired, get a new token
								Graph.authToken="";
								params["retries_expToken"]=(params["retries_expToken"])?params["retries_expToken"]+1:1; //count retries
								if (params["retries_expToken"]<3) {
									Graph.requestAuthCodeB(function(){Graph.fetchPosts(params);} ); 
								} else log("Graph.fetchPostsFQL: cannot refresh expired fb auth token",{level:3});
							}
							else if (data.error.message||null) log("Graph.fetchPosts: "+data.error.message,{level:3});

						} else 	log("Graph.fetchPostsFQL: response was unrecognized",{level:3});
						data=null;
				    	    } catch (e){log("Graph.fetchPostsFQL: response error: "+e+": "+response);}	
					} else log("Graph.fetchPostsFQL: response was empty",{level:3});
					if(req)req=null;
				}catch(e){log("Graph.fetchPostsFQL.onload: "+e);}},

				onabort: function(response) {try{
					//remove the memory that a request is out
					var r = Graph.matchRequest(params);
					if (r!=-1) Graph.requests.remove(r);
					log("Graph.fetchPostsFQL: aborted: "+response.responseText);
					if(req)req=null;
				}catch(e){log("Graph.fetchPostsFQL.onabort: "+e);}},

				ontimeout: function(response) {try{
					//remove the memory that a request is out
					params.timeouts++;
					var r = Graph.matchRequest(params);
					if (r!=-1) Graph.requests.remove(r);
					log("Graph.fetchPostsFQL: timeout: retry="+(params.timeouts<3)+", "+response.responseText);
					if(req)req=null;
					
					if (params.timeouts<3) Graph.fetchPosts(params);
				}catch(e){log("Graph.fetchPostsFQL.ontimeout: "+e);}},

				onerror: function(response) {try{
					//remove the memory that a request is out
					var r = Graph.matchRequest(params);
					if (r!=-1) Graph.requests.remove(r);
					log("Graph.fetchPostsFQL: error: "+response.responseText);
					if(req)req=null;
				}catch(e){log("Graph.fetchPostsFQL.onerror: "+e);}}
			});		
		}catch(e){log("Graph.fetchPostsFQL: "+e);}},
		
		/* fetchPosts details:
			params = {
			 feed:<feed reference>, 
			 filter:<appID>, 
			 next:<url containing 'until'>, 
			 prev:<url containing 'since'>, 
			 callback:<where to ship the return data>, 
			 retries_noToken:<counter>, 
			 fetchQty:<number>,
			 specific:<specific range object>
			}
		*/
		
		fetchPosts: function(params){try{
			log("Graph.fetchPosts is deprecated. Use Graph.fetchPostsFQL.");
			return Graph.fetchPostsFQL(params);
		
		
			log("Graph.fetchPosts()",{level:1});
			params=params || {};
			params.timeouts=params.timeouts||0;
			
			var bypassMatchRequest = (params.range||null)?true:false;
			
			//remember the target position if this is a ranged search
			//the very first call we make is a "since" call, but all sequential calls are "until" calls due to FB's stupid pagination methods
			if (params.range||null){
				//log(params.range.since);
				if (params.range.since||null) params.targetUntil = params.range.since;
			}
			
			
			if (!Graph.authToken) {
				log("Graph.fetchPosts: no authToken, get one");
				params["retries_noToken"]=(params["retries_noToken"])?params["retries_noToken"]+1:1; //count retries
				if (params["retries_noToken"]<3) {
					Graph.requestAuthCodeB(function(){Graph.fetchPosts(params);} ); 
				} else {
					log("Graph.fetchPosts: cannot get new fb auth token",{level:3});
					return {getAuthTokenFailed:true};
				}
				return;
			}

			//check if there is a request already out with this fb id and matches the direction
			var r=Graph.matchRequest(params);
			if (!bypassMatchRequest) if (r!=-1){
				if (Graph.requests[r].next==null && Graph.requests[r].prev==null) {
					log("Graph.fetchPosts: the initial request for data has not been returned yet",{level:3});
					return {initRequestSlow:true};
				} else {
					log("Graph.fetchPosts: a request is already out for posts in that direction and has not returned",{level:3});
					return {requestAlreadyOut:true};
				}
			}

			//for each user specified feed source, get posts
			var feed=params.feed||null;
			var filter = (params.filter||"default");
			if (!(feed.filters[filter]||null)) feed.addFilter({id:filter}); //create filter instance if needed

			var URL=feed.url+"?date_format=U&limit="+((params.range||null)?250:params.fetchQty)+"&access_token="+Graph.authToken;
			
			//get older posts
			//verify that the feed "until" time does not violate olderLimit set by user
			if (params.next || ((params.range||null)?params.range.until||null:null) ){
				var until=(params.range||null)?params.range.until:feed.filters[filter].next.getUrlParam("until"); 
				//debug.print(["var until",until]);
				if (until||null){
					var limit=(params.limit||null); //this is not FB search limit keyword, this is a WM timelimit
					var timeNow=timeStamp();
					//no oldest post limit on range fetches
					if (params.range||null) limit=null;
					
					var fixTime = (until.length < 10)?(until+"000"):until;

					//debug.print(["var until:",until, until.length, fixTime])
					
					if (limit) {
						if ((timeNow-(fixTime)) > limit) {
							//log("Graph.fetchPosts("+params.feed.url+"): the user-set older limit of this feed has been reached",{level:2});
							return {olderLimitReached:true};
						}
					}
					URL+="&until="+fixTime;
				} else {
					log("Graph.fetchPosts("+params.feed.url+"): The previous result did not return pagination. Restarting fetching from current time.");
				}
			}
			//get newer posts
			//rules manager action fetchHours will be asking for a range staring at time X, so use range.since
			else if (params.prev || ((params.range||null)?params.range.since||null:null) ) {
				var since=(params.range||null)?params.range.since:feed.filters[filter].prev.getUrlParam("since");
				if (exists(since)) {
					URL+="&since="+since;
				}
			}

			//add a filter if there is one
			if (exists(params.filter)) URL+="&filter="+filter; //check using params.filter, do not use filter here or it may inject "default"

			log("Graph.fetchPosts: processing feed <a target='_blank' href='"+URL+"'>"+URL+"</a>");
			
			//remember this request
			Graph.requests.push({feed:params.feed, next:params.next, prev:params.prev, filter:filter});

			var req; req=GM_xmlhttpRequest({
				method: "GET",
				url: URL,
				timeout: Graph.fetchTimeout*1000,
				onload: function(response) {try{
					//show dev tools
					if (opts && debug && !isChrome) if (opts.devDebugGraphData) {
						var pkg=debug.print("Graph.fetchPosts.onload.devDebugGraphData: ");
						pkg.msg.appendChild(createElement("button",{type:"button",onclick:function(){
								//response.responseText.toClipboard();
								promptText(response.responseText);
							}},[
							createElement("img",{src:"http://i1181.photobucket.com/albums/x430/merricksdad/array.png",title:"Show Data",style:"width:16px;height:16px; vertical-align:bottom;"})
						]));
					}
					
					//remove the memory that a request is out
					var r = Graph.matchRequest(params);
					if (r!=-1) Graph.requests.remove(r);

					if (response){
					    try{
						//convert to JSON
						var data = JSON.parse(response.responseText);
						//add new posts to graph.posts
						if (exists(data.data)) {
							//log("response contains data");

							//alert(JSON.stringify(data.data));
	
							//store posts
							if (data.data.length) log("Graph.fetchPosts.onLoad: "+data.data.length+" posts received. Validating data...");
							else log("Graph.fetchPosts.onLoad: facebook returned an empty data set.");
							var gotMoreToDo=false;
							if ((params.targetUntil||null) && (data.data.length) && (data.paging.next)) {
								var lastPullOldestPost=data.paging.next.getUrlParam("until");
								//2013/9/7: known facebook limit maximum is 500, but we are fetching in 250's
								//have we maxed out AND is oldest returned post newer than what we asked for
								gotMoreToDo = (data.data.length>=250) && (lastPullOldestPost > params.targetUntil);
							}
							if (data.data.length) for (var i=data.data.length-1;i>=0;i--) {
								var post=data.data[i];

								Graph.validatePost({
									post:post,
									callback:params.callback||null,
									next:params.next
								});

							}
							if (gotMoreToDo) {
								log("Graph.fetchPosts.onload: was not able to get enough in one return, going back for more...");
								//clone the last set of params
								var newParams = mergeJSON(params);
								newParams.range={since:0,until:0}; //new instance to prevent byRef errors
								//update the range settings
								//newParams.range.since=data.paging.previous.getUrlParam("since");
								newParams.range.until=data.paging.next.getUrlParam("until");
								//log([params.range.since,newParams.range.since,newParams.range.until,timeStampNoMS()]);
								newParams.targetUntil = params.range.since; //remember the original passed oldest data to target
								//make the next request
								Graph.fetchPosts(newParams);
							}
							
							//start cleanup
							if (params.callback) delete params.callback;


							//capture the next and prev urls, but dont overwrite current known time boundaries
							if (data.paging||null){
								//if this is the first time we've used this object, remember its locations
								if (!feed.filters[filter].next) feed.filters[filter].next = data.paging.next;
								if (!feed.filters[filter].prev) feed.filters[filter].prev = data.paging.prev;

								//if the current request did not get older posts, push the newer post bracket
								if (!params.prev) feed.filters[filter].next = data.paging.next;
								//if the current request did not get newer posts, push the older post bracket
								if (!params.next) feed.filters[filter].prev = data.paging.previous;
							} else {
								log("Graph.fetchPosts.onLoad: facebook failed to return pagination data.")
							}

						} else if (data.error||null) {
							//check for session expired
							if ((data.error.message||"").find("Session has expired")){
								//session expired, get a new token
								Graph.authToken="";
								params["retries_expToken"]=(params["retries_expToken"])?params["retries_expToken"]+1:1; //count retries
								if (params["retries_expToken"]<3) {
									Graph.requestAuthCodeB(function(){Graph.fetchPosts(params);} ); 
								} else log("Graph.fetchPosts: cannot refresh expired fb auth token",{level:3});
							}
							else if (data.error.message||null) log("Graph.fetchPosts: "+data.error.message,{level:3});

						} else 	log("Graph.fetchPosts: response was unrecognized",{level:3});
						data=null;
				    	    } catch (e){log("Graph.fetchPosts: response error: "+e+": "+response);}	
					} else log("Graph.fetchPosts: response was empty",{level:3});
					if(req)req=null;
				}catch(e){log("Graph.fetchPosts.onload: "+e);}},

				onabort: function(response) {try{
					//remove the memory that a request is out
					var r = Graph.matchRequest(params);
					if (r!=-1) Graph.requests.remove(r);
					log("Graph.fetchPosts: aborted: "+response.responseText);
					if(req)req=null;
				}catch(e){log("Graph.fetchPosts.onabort: "+e);}},

				ontimeout: function(response) {try{
					//remove the memory that a request is out
					params.timeouts++;
					var r = Graph.matchRequest(params);
					if (r!=-1) Graph.requests.remove(r);
					log("Graph.fetchPosts: timeout: retry="+(params.timeouts<3)+", "+response.responseText);
					if(req)req=null;
					
					if (params.timeouts<3) Graph.fetchPosts(params);
				}catch(e){log("Graph.fetchPosts.ontimeout: "+e);}},

				onerror: function(response) {try{
					//remove the memory that a request is out
					var r = Graph.matchRequest(params);
					if (r!=-1) Graph.requests.remove(r);
					log("Graph.fetchPosts: error: "+response.responseText);
					if(req)req=null;
				}catch(e){log("Graph.fetchPosts.onerror: "+e);}}
			});
		}catch(e){log("Graph.fetchPosts: "+e);}},
	};

	log("Graph initialized");
})();