Metafilter mark contact contributions

By Plutor Last update May 22, 2008 — Installed 865 times.
// ==UserScript==
// @name           Metafilter mark contact contributions
// @namespace      http://plutor.org/
// @description    Mark contact contributions on Metafilter
// @include        http://metafilter.com/*
// @include        http://*.metafilter.com/*
// ==/UserScript==

/* -- Configuration variables, sort of -------------------------------------- */

var showcontacts = 1;            // Boolean

var updateevery = 86400 * 1000;         // One day
var debug = 0;

var contactimgs = {
    // 1 - link to contact, star
    'linkto': "data:image/jpeg;base64,R0lGODlhEAAMAKUAACcnCgwMA3l5HlNTFQAAAMnJMqOjKBcXBfb2OObmNAMDAUFBC1ZWD4yMGe/vLWdnEkJCCysrBxcXBKmpGebmIZeXFxERAwICAGhoC9nZFt3dFs7OFU9PCWFhBdTUCjw8A42NAc3NAWxsAQoKAMXFAL6+AEJCAGlpAMvLAKqqAEVFAJqaABgYADo6ALu7ABwcACsrABcXADIyAP///////////////////////////////////////////////////yH5BAEKAD8ALAAAAAAQAAwAAAZSwJ9w+AMEiEikYJBsEgqGZvKASCikC4a24XA8IOAIUTKhmM/migV5wWQ0cM2GI/11PHjPp/4Dhf4hInUjJCUmJygpdSorLEItLi9SMBdEMTJEQQA7",
    // 2 - link from contact, grey star
    'linkfrom': "data:image/jpeg;base64,R0lGODlhEAAMAOcAAAAAAAEBAQICAgMDAwQEBAUFBQYGBgcHBwgICAkJCQoKCgsLCwwMDA0NDQ4ODg8PDxAQEBERERISEhMTExQUFBUVFRYWFhcXFxgYGBkZGRoaGhsbGxwcHB0dHR4eHh8fHyAgICEhISIiIiMjIyQkJCUlJSYmJicnJygoKCkpKSoqKisrKywsLC0tLS4uLi8vLzAwMDExMTIyMjMzMzQ0NDU1NTY2Njc3Nzg4ODk5OTo6Ojs7Ozw8PD09PT4+Pj8/P0BAQEFBQUJCQkNDQ0REREVFRUZGRkdHR0hISElJSUpKSktLS0xMTE1NTU5OTk9PT1BQUFFRUVJSUlNTU1RUVFVVVVZWVldXV1hYWFlZWVpaWltbW1xcXF1dXV5eXl9fX2BgYGFhYWJiYmNjY2RkZGVlZWZmZmdnZ2hoaGlpaWpqamtra2xsbG1tbW5ubm9vb3BwcHFxcXJycnNzc3R0dHV1dXZ2dnd3d3h4eHl5eXp6ent7e3x8fH19fX5+fn9/f4CAgIGBgYKCgoODg4SEhIWFhYaGhoeHh4iIiImJiYqKiouLi4yMjI2NjY6Ojo+Pj5CQkJGRkZKSkpOTk5SUlJWVlZaWlpeXl5iYmJmZmZqampubm5ycnJ2dnZ6enp+fn6CgoKGhoaKioqOjo6SkpKWlpaampqenp6ioqKmpqaqqqqurq6ysrK2tra6urq+vr7CwsLGxsbKysrOzs7S0tLW1tba2tre3t7i4uLm5ubq6uru7u7y8vL29vb6+vr+/v8DAwMHBwcLCwsPDw8TExMXFxcbGxsfHx8jIyMnJycrKysvLy8zMzM3Nzc7Ozs/Pz9DQ0NHR0dLS0tPT09TU1NXV1dbW1tfX19jY2NnZ2dra2tvb29zc3N3d3d7e3t/f3+Dg4OHh4eLi4uPj4+Tk5OXl5ebm5ufn5+jo6Onp6erq6uvr6+zs7O3t7e7u7u/v7/Dw8PHx8fLy8vPz8/T09PX19fb29vf39/j4+Pn5+fr6+vv7+/z8/P39/f7+/v///yH5BAEKAP8ALAAAAAAQAAwAAAhmAP8JHPgvQQCCCBF6WZGwIYBsqBomHJAPHgCJGlxo9FOvnhANIBcQHPDqncmTJiMdJAhACDl0MNFpMyHxH45vOL9VqPlPz7Wf147UBPDsWAYh1VjV3CBpgMAJwwhIVHBx4AAHBAMCADs%3D",
    // 3 - two-way link, double star
    'mutuallink': "data:image/jpeg;base64,R0lGODlhEQAMAKUAACcnCgwMA3l5HlNTFQAAAMnJMqOjKBcXBfb2OObmNAMDAUFBC1ZWD4yMGe/vLWdnEkJCCysrBxcXBKmpGebmIZeXFxERAwICAGhoC9nZFt3dFs7OFU9PCWFhBdTUCjw8A42NAc3NAWxsAQoKAMXFAL6+AEJCAGlpAMvLAKqqAEVFAJqaABgYADo6ALu7ABwcACsrABcXADIyAP///////////////////////////////////////////////////yH5BAEKAD8ALAAAAAARAAwAAAZpwJ9wCAj8isOkUjD4MZVQQsEgNUCVB0RCoVVAF4xww+F4QM4RtFAyobjf7oqFIh9eMBmNXrPh8DlXHR6DHh+DH1c/ICGMISKMIlcjJCUmJygplylXKissnS0uL6EvUDAXP6c/MTKrrUJBADs%3D",
    'nearby': "data:image/gif,GIF89a%10%00%0C%00%C6%5C%00%00%00%00%01%00%00%03%02%01%05%01%01%05%02%01%06%02%01%09%02%02%0A%03%02%08%04%03%09%04%03%0B%04%03%0C%04%03%0D%05%04%0F%05%03%11%05%03%0F%06%04%12%06%03%13%06%04%11%07%05%13%08%05%15%08%05%15%08%06%17%08%05%18%08%05%17%09%07%17%0A%073%10%09%3A%11%099%12%0AG%16%0BO%19%0Eb%20%13r%25%14m3(%86*%17%906%23%A52%1A%B38%1E%BB7%1C%96H9%BF%3B%20%D0%40%22%D8%40!%E2C%22%E4D%23%EAE%23%EAF%23%EBF%24%ECF%23%ECF%24%EFG%24%EFH%25%EFJ'%ECQ0%F0Q0%F0Q1%F0R2%EBT5%CCfP%F0Y%3A%F1%60B%D0p%5D%F1hL%CE%7Ck%EBqW%D9zf%F2y_%F3%80h%F3%82k%F3%83k%F4%85n%F4%86p%F5%99%86%E9%A5%97%F6%A1%90%F5%A2%91%F6%A4%93%F6%A9%99%F7%AB%9C%F3%B1%A3%EB%B6%AB%F8%B8%AA%F3%BB%B0%F6%BC%B0%F8%BD%B1%F9%C5%BA%FD%F2%F0%FD%F5%F3%FD%F8%F7%FE%FB%FB%FE%FE%FD%FE%FE%FE%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF!%F9%04%01%0A%00%7F%00%2C%00%00%00%00%10%00%0C%00%00%07%7D%80%7F%82%83%7F%07%03%84%88%84%04A%23%89%88%04%18%19)%1E%08%12%0A%8E%7F'SO4%40IR%3D%0E%89%10P%5BV7Q%5BUK%20%89!X%5B%5BWZY%3EL%3A%05%84%02%3F%B1%BDT38M%1C%84%01%1FBCDFE6%26%2C%22%06%88%119N2J%3C0%17%24%1D%89%165H2G%3B.%13(%1B%89%0D*-11%2F%2B%0B%25%1A%8E%04%0C%15%14%11%0F%00%09%01%83%81%00%3B"
};

var CONFIG = {
    "you":          'none',
    "linkto":       'icon',
    "linkfrom":     'icon',
    "mutuallink":   'icon',
    "nearby":       'icon'
};
var USERTYPES = { "you":        "You",
                  "linkto":     "Your contacts",
                  "linkfrom":   "Contact you",
                  "mutuallink": "Mutual contacts",
                  "nearby":     "Nearby" };
var CONFIGVALS = {
    "icon":  0,
    "color": 1,
    "style": 1,
    "none":  0
}; // Name => has text field
var NEARBY_MAX_DIST = 10;    // Miles

/* -- Global variables ------------------------------------------------------ */

var userid = mcc_get_cookie("USER_ID");
var clist = new Object();
var now = new Date();

/* -- Simple helper functions ----------------------------------------------- */

function mcc_is_logged_in() {
        if (!userid || userid <= 0) return 0;
        return 1;
}

function mcc_get_cached_data() {
    try {
        clist = eval(GM_getValue("MefiContacts", ""));
    } catch(e) {
        GM_log("Error parsing cache");
        clist = new Object();
    }
}

function mcc_last_cache_update() {
    if (!clist) return 0;
    if (!clist.stamp) return 0;

    if (debug) GM_log("Last updated at " + clist.stamp, 0);
    return clist.stamp;
}

/* -- XMLHTTP request ------------------------------------------------------- */

function mcc_update_contact_list() {
    /* GET http://www.metafilter.com/usercontacts/USERID */
    var url = 'http://www.metafilter.com/usercontactsdata.mefi?user_id=' + userid;
    if (debug) GM_log("Getting " + url, 0);

    GM_xmlhttpRequest({
        method: "GET",
        url:    url,
        onload: mcc_got_contact_list });
}

function mcc_got_contact_list(details) {
    if (details.readyState == 4 && details.status == 200) {
        var xml = new DOMParser().parseFromString(details.responseText, 'text/xml');
        mcc_handle_contact_list(xml);
    }
}

/* -- Creates the list from the contact list page --------------------------- */

function mcc_handle_contact_sublist(root, tagname) {
    var contacts = root.getElementsByTagName(tagname);

    if (contacts) {
        contacts = contacts[0].childNodes;

        for (var i=0; i<contacts.length; ++i) {
            var contact = contacts[i];
            if (contact.tagName && contact.tagName.toUpperCase() == 'USER') {
                var rel, id, name, dist;
                try {
                    id = contact.getElementsByTagName('id')[0].childNodes[0].nodeValue;
                    name = contact.getElementsByTagName('username')[0].childNodes[0].nodeValue;
                } catch (e) {
                    // Can't find all the required bits
                    continue;
                }

                try {
                    rel = contact.getElementsByTagName('relationship')[0].childNodes[0].nodeValue;
                } catch(e) {}

                try {
                    dist = contact.getElementsByTagName('distance')[0].childNodes[0].nodeValue;
                } catch(e) {}

                var newc;
                if (clist.uid[id]) {
                    newc = clist.uid[id];
                } else {
                    newc = { id: id, name: name };
                }
                if (dist) newc.dist = dist;
                if (rel) {
                    if (newc.rel)
                        newc.rel += " " + rel
                    else
                        newc.rel = rel;
                }
                newc[tagname] = 1;

                clist.uid[id] = newc;
                clist.name[name] = newc;
            }
        }
    }
}

function mcc_handle_contact_list(xml) {
    var root = xml.documentElement;

    var numcontacts = 0;
    clist = { uid: {}, name: {} };
    
    mcc_handle_contact_sublist(root, 'contacts');
    mcc_handle_contact_sublist(root, 'contactsfrom');
    mcc_handle_contact_sublist(root, 'nearby');

    /* Save */
    clist.stamp = now.valueOf();
    GM_setValue("MefiContacts", clist.toSource());

    /* Now mark contributions */
    mcc_mark_contribs();
}

/* -- Helper function for mcc_handle_contact_list --------------------------- */

/* blob - The HTML of a list of contacts */
/* returns a list of contacts in that blob */
function handleCListGetNames(blob) {
        var list = new Array();

        var listre = new RegExp("<a href=\"/user/([^\"]*)\"[^>]*>([^<]*)", "gi");
        var matches = blob[0].match( listre );

        if (matches) {
                for (i=0; i<matches.length; ++i) {
                        var j = matches[i];
                        var n = j.match(/\d+/);

                        if (n) {
                                list.push(n[0]);
                        }
                        list.push(j.substr(j.lastIndexOf(">")+1));
                }
        }

        return list;
}

/* -- Marks contribs from contacts  ----------------------------------------- */

var alreadymarked = 0;
function mcc_mark_contribs() {
    if (alreadymarked) return;
    var t0 = new Date();

    var userhrefre = new RegExp("^(http://(www\.)?metafilter\.com)?/user(\.mefi)?/");
    var usernamehrefre = new RegExp("(http://(www\.)?metafilter\.com)?/username.mefi/");

    for (var i in document.links) {
        var l = document.links[i];
        var lh = new String(l.href);
        var key = lh.substr(lh.lastIndexOf("/")+1);
        var obj = null;

        if (lh.match(userhrefre)) {
            obj = clist.uid[key];
        }
        else if (lh.match(usernamehrefre)) {
            obj = clist.name[key];
        }

        if (obj) {
            var type = "";
            if (obj.contacts && obj.contactsfrom)       { type = 'mutuallink'; }
            else if (obj.contacts && !obj.contactsfrom) { type = 'linkto'; }
            else if (!obj.contacts && obj.contactsfrom) { type = 'linkfrom'; }
            else if (key == userid && l.innerHTML != "My Profile") { type = 'you'; }

            if (type && CONFIG[type]) {
                if (CONFIG[type] == 'icon' && contactimgs[type]) {
                    var ni = document.createElement("img");
                    ni.src = contactimgs[type];
                    ni.style.borderWidth = '0';
                    l.parentNode.insertBefore(ni, l);
                } else if (CONFIG[type].match('^color')) {
                    var val = CONFIG[type].replace(/^color /, '');
                    l.style.color = val;
                } else if (CONFIG[type].match('^style')) {
                    var val = CONFIG[type].replace(/^style /, '');
                    l.style.cssText = val;
                }
            }

            if ((obj.dist != null) && CONFIG['nearby'] &&
                parseInt(obj.dist) <= parseInt(NEARBY_MAX_DIST)) {
                if (CONFIG['nearby'] == 'icon') {
                    var title = "This user lives " + obj.dist + " miles away from you";
                    var ni = document.createElement("img");
                    ni.src = contactimgs['nearby'];
                    ni.title = title;
                    ni.style.borderWidth = '0';
                    l.parentNode.insertBefore(ni, l);
                } else if (CONFIG['nearby'].match('^color')) {
                    var val = CONFIG['nearby'].replace(/^color /, '');
                    l.style.color = val;
                } else if (CONFIG['nearby'].match('^style')) {
                    var val = CONFIG['nearby'].replace(/^style /, '');
                    l.style.cssText = val;
                }
            }
        }
    }

    alreadymarked = 1;
        if (debug) GM_log("Took " + (new Date() - t0) + "ms to mark");
}

/* -- Get cookie value (for USER_ID) ---------------------------------------- */

/* From http://www.netspade.com/articles/javascript/cookies.xml */
function mcc_get_cookie(name) {
    var dc = document.cookie;
    var prefix = name + "=";
    var begin = dc.indexOf("; " + prefix);
    if (begin == -1)
    {
        begin = dc.indexOf(prefix);
        if (begin != 0) return null;
    }
    else
    {
        begin += 2;
    }
    var end = document.cookie.indexOf(";", begin);
    if (end == -1)
    {
        end = dc.length;
    }
    return unescape(dc.substring(begin + prefix.length, end));
}

/* ======================================================================== */

function mcc_load_preferences() {
    for (type in USERTYPES) {
        CONFIG[type] = GM_getValue('mcc_' + type, CONFIG[type]);
    }

    var nmd = GM_getValue('mcc_nearby_max_dist', NEARBY_MAX_DIST);
    if (nmd)
        NEARBY_MAX_DIST = nmd;
}

function mcc_save_preferences() {
    for (type in USERTYPES) {
        var name = 'mcc_' + type + '_config';

        var select_el = document.getElementById(name);
        var select_val = select_el.value;

        var text_id = 'mcc_' + type + '_' + select_val;
        var text_el = document.getElementById(text_id);

        if (text_el) {
            select_val = select_val + " " + text_el.value;
        }
    
        GM_setValue('mcc_' + type, select_val);
    }

    var nearby_max_dist = document.getElementById('mcc_nearby_max_dist').value;
    GM_setValue('mcc_nearby_max_dist', nearby_max_dist);

    return true; /* So it actually submits, too */
}

/* ======================================================================== */

function mcc_escape(str) {
    return str.replace(/"/g, '&quot;');
}

/* ======================================================================== */

function mcc_init_preferences() {
    var inputs = document.getElementsByTagName('input');
    var submit_button;

    // Find the submit button
    for (var i=0; i<inputs.length; ++i) {
        if (inputs[i].type == 'submit' && inputs[i].value.match(/Save your Preferences/)) {
            submit_button = inputs[i];
            break;
        }
    }
    if (!submit_button) return;

    // Create the fieldset
    var mcc_fieldset = document.createElement('fieldset');
    var mfftext = '<legend>Contact contribution display preferences</legend>';

    for (type in USERTYPES) {
        var name = 'mcc_' + type + '_config';

        mfftext += '<label for="'+ name + '">' + USERTYPES[type] + ': </label>'
                + '<select id="'+ name + '" name="'+ name + '" style="margin-left:0px;" onfocus="this.style.background=\'#ddd\';" onblur="this.style.background=\'#ccc\';">';

        for (j in CONFIGVALS) {
            var selected = (CONFIG[type].match("^" + j)) ? " selected" : "";
            mfftext += '<option value="' + j + '"' + selected + '>' + j + '</option>'
        }

        mfftext += '</select> ';

        for (j in CONFIGVALS) {
            if (CONFIGVALS[j]) {
                var display = (CONFIG[type].match("^" + j)) ? "inline" : "none";
                var id = 'mcc_' + type + '_' + j;
                var val = "";
                if (display != "none") {
                    val = mcc_escape(CONFIG[type].replace(RegExp("^" + j + " "), ''));
                }
                mfftext += '<input type="text" id="' + id + '" size="40" '
                        + 'style="display: ' + display + '; margin: 0;" value="' + val + '">'
            }
        }

        //mfftext += '<br>';
	    mfftext += '<div style="clear: both;"></div>';
    }

    mfftext += '<label for="mcc_nearby_max_dist">Nearby user threshold: </label>'
    	+ '<input type="text" id="mcc_nearby_max_dist" name="mcc_nearby_max_dist" value="'
        + NEARBY_MAX_DIST
        + '" maxlength="4" size="4" onfocus="this.style.background=\'#ddd\';" onblur="this.style.background=\'#ccc\';" /> <span class="smallcopy">miles</span><br />';

    mcc_fieldset.innerHTML = mfftext;
    submit_button.parentNode.insertBefore( mcc_fieldset, submit_button );

    // Add change listeners
    for (type in USERTYPES) {
        var name = 'mcc_' + type + '_config';
        var el = document.getElementById(name);
        if (el)
            el.addEventListener( 'change', mcc_pref_show_text, true );
    }

    // Add javascript to the form
    submit_button.form.addEventListener( 'submit', mcc_save_preferences, true );
}

/* Show the right text box (if any) for the dropdown */
function mcc_pref_show_text(e) {
    if (!this) return;
    var sel = this;
    var sel_id = sel.id;
    var type = (sel_id.split("_"))[1];
    var val = sel.options[sel.selectedIndex].value;

    for (j in CONFIGVALS) {
        if (CONFIGVALS[j]) {
            var textel = document.getElementById('mcc_' + type + '_' + j);
            var display = (val == j) ? "inline" : "none";
            textel.style.display = display;
        }
    }
}

/* -- Main ------------------------------------------------------------------ */

function mcc_init() {
    if (!mcc_is_logged_in()) return;

    /* Get cached data */
    mcc_get_cached_data();
    mcc_load_preferences();

    /* Update cache if we're at the contact list */
    /* FIXME - Also update if we're adding a contact (submitting a form to xfnaction.cfm) */

    var url = location.href;
    url = url.replace(/https?:\/\/([^\/]*\.)?metafilter.com/, '');

    if (url.match('^/contribute/customize.cfm')) {
        /* Show preferences list */
        mcc_init_preferences();
    } else if (now - mcc_last_cache_update() > updateevery) {
        /* Download page and update cache if it's old */
        mcc_update_contact_list();
    } else {
        /* Otherwise, just mark the contributions immediately */
        mcc_mark_contribs();
    }
}

/* DO IT */
mcc_init();