Greasy Fork is available in English.

Social Friend Tracker

Monitors your Facebook frends and notifies you if you are unfriended

/*
 * Social Friend Tracker
 * 
 * NOTICE: this user script contains some code nicked from Social Fixer by
 * Matt Kruse.  With apologies.
 *
 * Social Fixer used to contain a Friend Tracker.  However, since SF 8.0
 * this has no longer been the case because Facebook objected and placed
 * pressure on the author to remove it.  This script is an attempt to 
 * resurrect the Friend Tracker.
 */

// ==UserScript==
// @name           Social Friend Tracker
// @namespace      http://userscripts.org/users/49156
// @description    Monitors your Facebook frends and notifies you if you are unfriended
// @include        http://*.facebook.com/*
// @include        http://facebook.com/*
// @include        https://*.facebook.com/*
// @include        https://facebook.com/*
// @grant          unsafeWindow
// @grant          GM_getValue
// @grant          GM_setValue
// @grant          GM_xmlhttpRequest
// @grant          GM_log
// @version        0.85
// ==/UserScript==
/*
 * 0.80 24-dec-2013 was the initial release
 * 0.81 30-dec-2013 fix the regexp that parses the user_num out of cookies
 * 0.82 31-mar-2014 fb changed the newsfeed and the box disappeared, but
                    only because of a silly typo in the code
 * 0.83 04-aug-2015 @grant (required for GreaseMonkey 2+)
 * 0.84 18-feb-2017 when refriended, don't overwrite the refriended data if
                    it already existed, so we remember the original timestamp
 * 0.85 08-jan-2019 add fb_dtsg token because the API stopped working without it
 */
 
var addGlobalStyle = function(css) {
    var head, style;
    head = document.getElementsByTagName('head')[0];
    if (!head) { return; }
    style = document.createElement('style');
    style.type = 'text/css';
    style.innerHTML = css;
    head.appendChild(style);
}
addGlobalStyle(
    '#sft_pagelet {border: solid 1px; padding: 2px;}' +
    '.sft_noactivity {font-weight:bold;}' +
    '.sft_unfriended {color: #ff0000;}' +
    '.sft_friended {color: #00ff00;}' +
    '.sft_unfriend_name {margin-left:1em;}' +
    '.sft_friend_name {margin-left:1em;}'
);

var queueFunction = function(f) {return setTimeout(f,0);};
var time = function () {return (new Date()).getTime();};
var protocol='http:'; try { protocol = location.protocol; } catch(e) { }
var host='facebook.com';try { host = location.host; } catch(e) { }
var user_num = null;try {user_num = unsafeWindow.Env.user;} catch(e) { }
if (!user_num) try {user_num=document.cookie.match(/^(.*;\s*)?c_user=([0-9]+)(;.*)?$/)[2];} catch(e) { }
var fb_dtsg = document.getElementsByName('fb_dtsg')[0].value;

var requestProps = {type:'xhr', url:protocol+'//'+host+'/ajax/typeahead/first_degree.php?__a=1&filter[0]=user&lazy=0&options[0]=friends_only&viewer='+user_num+'&__user='+user_num+'&fb_dtsg='+fb_dtsg, headers:{'Content-type':'application/x-www-form-urlencoded'}, ttl:3600 };

var getData = function (func,force) {
    if (!user_num) return;
    var t=time();
    var rc=requestProps;
    var dt = user_num + '.data';
    if (!force) {
        var lastcheck = +GM_getValue(dt+'.last_check',0);
        if (t-lastcheck <= rc.ttl*1000) {
            var cache = GM_getValue(dt,'');
            if (cache) { func(cache); return; }
        }
    }
    if (!rc.loading) {
        rc.loading=true;
        var headers=rc.headers;
        headers['Cache-Control']='no-cache';
        var url=rc.url;
        url += '&time='+t;
        try {
            GM_xmlhttpRequest({'method': 'GET', 'headers': headers, 'url': url, 'onload': function(res) {
                rc.loading=false;
                if (res.responseText==null || res.responseText=="") {
                    GM_log("ajax request returned no content");
                    return;
                }
                val=func(res.responseText);
                if (val!=false) {
                    GM_setValue(dt+'.last_check',''+t);
                    GM_setValue(dt,res.responseText);
                }
            }, 'onerror': function(res) {
                GM_log("AJAX stopped with error");
                GM_log("Status: " + res.statusText);
            }});
        } catch (e) {
            GM_log("An error occurred during the ajax request: " + e.toString());
        }
    }
};
var object_to_array = function(obj,key,desc) {
    var dir=1; if (desc) dir=-1;
    var a=[];
    for(var i in obj) {a.push(obj[i]);}
    return a.sort(function(a,b){
        var ta=typeof a[key]; var tb=typeof b[key];
        if (ta=="undefined" && tb=="undefined") return 0;
        if (tb=="undefined") return dir;
        if (ta=="undefined") return -dir;
        if (a[key]>b[key]) return dir;
        if (a[key]==b[key]) return 0;
        return -dir;
    });
};
var ago = function(when,now) {
    var diff = Math.floor((now-when)/1000/60);
    if (diff<60) return diff+" min ago";
    diff = Math.floor(diff/60);
    if (diff<24) return diff+" hr ago";
    diff = Math.floor(diff/24);
    return diff+" days ago";
};
var sftClearPressed = false;
var sftLoaded = false;
var sftProcessData = function(data) {
    var dirty = false;
    var t = time();
    var dt = user_num + '.friends';
    var fdata = GM_getValue(dt,"");
    var friends = {};
    if (fdata != "") {
        try { friends = JSON.parse(fdata); } catch (e) { GM_log("Friends JSON data could not be parsed"); }
    }
    if (typeof friends.friends=="undefined") { friends.friends={}; }
    if (typeof friends.unfriended=="undefined" || sftClearPressed) { friends.unfriended={}; }
    if (typeof friends.refriended=="undefined" || sftClearPressed) { friends.refriended={}; }
    if (sftClearPressed) dirty=true;
    sftClearPressed = false;
    var old_friends = friends.friends;
    var unfriended = friends.unfriended;
    var refriended = friends.refriended;
    var count=0;
    var friend_list = {};
    try { friend_list = JSON.parse(data.replace(/for\s*\(\s*\;\s*\;\s*\)\s*\;/,'')); }
    catch (e) { GM_log("Friends data returned from Facebook could not be parsed"); return false; }
    if (!friend_list.payload || !friend_list.payload.entries) { return false; }
    friend_list = friend_list.payload.entries;
    if (friend_list && friend_list.length>5) {
        sftLoaded = true;
        var current_friends = {};
        /* analyse each current friend in the returned data */
        for (var i=0;i<friend_list.length;i++) {
            if (friend_list[i].type=="user") {
                count++;
                var id = friend_list[i].uid;
                if (id == user_num) continue;
                var name = friend_list[i].text;
                var f = {'name':name,'added':t};
                if (typeof old_friends[id]=="undefined") {
                    old_friends[id] = f; /* this is a new friend */
                    dirty = true;
                }
                current_friends[id] = f;
                if (typeof unfriended[id]!="undefined" && typeof refriended[id]  == "undefined") {
                    /* this friend was unfriended, but has returned */
                    refriended[id] = unfriended[id];
                    refriended[id].refriended = t;
                    refriended[id].id = id;
                    dirty = true;
                }
            }
        }
        /* now look for old friends who didn't appear in the data */
        for (var id in old_friends) {
            if (id==user_num) continue;
            if (typeof current_friends[id]=="undefined") {
                /* Gone! */
                unfriended[id] = old_friends[id];
                unfriended[id].deleted = t;
                unfriended[id].id = id;
                delete old_friends[id];
                delete refriended[id];
                dirty = true;
            }
        }
        var dtk=user_num + '.keep_days';
        var days=+GM_getValue(dtk,'5');
        var duration = 1000*60*60*24*days;
        var timeClass = " uiStreamSource timestamp ";
        /* Report each current unfriend unless too old */
        var unmsg="";
        var unfriend_array = object_to_array( unfriended, 'deleted', true );
        for (var i=0; i<unfriend_array.length; i++) {
            var f = unfriend_array[i];
            var id = f.id;
            if (t-f.deleted > duration ) {
                delete unfriended[id];
                dirty = true;
            } else {
                if (unmsg=="") unmsg='You are <span class="sft_unfriended">no longer friends</span> with:';
                unmsg += '<div><a href="/profile.php?id='+id+'" target="_blank" class="sft_unfriend_name">'+f.name+'</a> ' +
                         '<span class="'+timeClass+'">'+ago(f.deleted,t)+' <span class="sft_unfriend_reason"></span></span></div>';
            }
        }
        /* Report each current refriend unless too old */
        var remsg="";
        var refriend_array = object_to_array( refriended, 'refriended', true );
        for (var i=0; i<refriend_array.length; i++) {
            var f = refriend_array[i];
            var id = f.id;
            if (t-f.refriended > duration ) {
                delete refriended[id];
                dirty = true;
            } else {
                if (remsg=="") remsg='You have been <span class="sft_friended">re-friended</span> by:';
                remsg += '<div><a href="/profile.php?id='+id+'" target="_blank" class="sft_friend_name">'+f.name+'</a> ' +
                         '<span class="'+timeClass+'">'+ago(f.refriended,t)+'</span></div>';
            }
        }
        var pagelet = document.getElementById('sft_pagelet');
        if (unmsg!="" || remsg!="") {
            var keepmsg = '<div class=sft_keep_msg>(These names will be remembered for <input id=sft_keep_days type=text size=1 value="'+days+'"> days or until the Clear button is pressed.)</div>';
            pagelet.innerHTML = unmsg + remsg + keepmsg;
            /* monitor the keep_days input for changes */
            var elt=pagelet.querySelector('#sft_keep_days');
            if (elt) elt.onchange=function () {
                var days=document.getElementById('sft_keep_days').value;
                if (+days>0) GM_setValue(dtk,''+days);
            };
            /* Find out whether the unfriended accounts have been deactivated */
            var elts=pagelet.querySelectorAll('.sft_unfriend_name');
            if (elts && elts.length>0){
                for (var i=0; i<elts.length; i++) {
                    var a=elts[i];
                    GM_xmlhttpRequest({'method': 'GET', 'url':a.href,'onload':function(res){
                        if (res.status==404) {
                            elt=a.parentNode.querySelector('.sft_unfriend_reason');
                            if (elt) elt.innerHTML='(account inactive)';
                        }
                    }});
                }
            }
        } else {
            pagelet.innerHTML = '<div class="sft_noactivity">No activity (When you are unfriended, names will show up here)</div>';
        }
        /* Update the friend count in the box header */
        var elt = document.getElementById('sft_friend_number');
        if (elt) elt.innerHTML=' (' + (count-1) + ')';
        if (dirty) {
            GM_setValue(dt,JSON.stringify(friends));
        }
    }
    return true;
};
var sftFail = function() {
    if (sftLoaded) return;
    var pagelet = document.getElementById('sft_pagelet');
    if (pagelet) {
        if (!user_num) pagelet.innerHTML='Friend Tracker was unable to determine your Facebook user number.';
        else pagelet.innerHTML='Friend Tracker failed to load.  Please check browser\'s setting for third party cookies.';
    }
};
var sftLoadContent = function(force) {
    var pagelet = document.getElementById('sft_pagelet');
    if (pagelet) pagelet.innerHTML="Loading...";
    setTimeout(sftFail,10000);
    getData(sftProcessData,force);
};
var insertAfter = function(newelt,child) {
    var parent=child.parentNode;
    var sibling=child.nextSibling;
    if (sibling) parent.insertBefore(newelt,sibling);
    else parent.appendChild(newelt);
};
var sftMakePagelet = function() {
    var rightCol = document.getElementById('rightCol');
    if (!rightCol) return 0;
    var div = document.createElement('DIV');
    div.id='sft_pagelet_container';
    div.innerHTML='<div class="mbl">'+
                   '<div class="uiHeader uiHeaderBottomBorder uiHeaderTopAndBottomBorder uiSideHeader mbm mbs pbs">'+
                    '<div class="clearfix uiHeaderTop">'+
                     '<div class="uiTextSubtitle uiHeaderActions rfloat" id=sft_button_refresh><a href="#">Refresh</a>&nbsp;</div>'+
                     '<div class="uiTextSubtitle uiHeaderActions rfloat" id=sft_button_clear><a href="#">Clear</a>&nbsp;</div>'+
                     '<div>'+
                      '<h4 class="uiHeaderTitle pagelet_title">Friend Tracker</h4><span id="sft_friend_number"></span>'+
                     '</div>'+
                    '</div>'+
                   '</div>'+
                   '<div class="UIRequestBox phs">'+
                    '<div id="sft_pagelet" class="UIImageBlock clearfix UIRequestBox_Request UIRequestBox_RequestFirst UIRequestBox_RequestOdd">'+
                    '</div>'+
                   '</div>'+
                  '</div>';
    var set_onclick = function(parent, id, func) { var elt = parent.querySelector('#'+id); if (elt) elt.onclick=func; }
    set_onclick(div, 'sft_button_refresh', function () {sftLoadContent(true);});
    set_onclick(div, 'sft_button_clear', function () {sftClearPressed=true; sftLoadContent();});
    var rem = document.getElementById('pagelet_reminders');
    if (rem) insertAfter(div,rem);
    else {
        var container=rightCol;
        var get = function(selector){
            var x = container.querySelector(selector);
            if (x) container=x;
        }
        get('.home_right_column');
        get('.rightColumnWrapper');
        var first=container.firstChild;
        if (first) insertAfter(div,first);
        else container.appendChild(div);
    }
    queueFunction(sftLoadContent);
    return 1;
};

queueFunction(sftMakePagelet);