Sleazy Fork is available in English.

FetLife Spyscope

Hover over FetLife user avatar pictures to see their recent activity, vitals, group subscriptions, and more. Quickly discern whether they're worth talking back to or not.

/**
 *
 * This is a Greasemonkey script and must be run using a Greasemonkey-compatible browser.
 *
 * @author maymay <bitetheappleback@gmail.com>
 */
// ==UserScript==
// @name           FetLife Spyscope
// @version        0.1.1
// @namespace      http://maybemaimed.com/playground/fetlife-spyscope/
// @description    Hover over FetLife user avatar pictures to see their recent activity, vitals, group subscriptions, and more. Quickly discern whether they're worth talking back to or not.
// @include        https://fetlife.com/*
// @exclude        https://fetlife.com/adgear/*
// @exclude        https://fetlife.com/chat/*
// @exclude        https://fetlife.com/im_sessions*
// @exclude        https://fetlife.com/polling/*
// @exclude        https://fetlife.com/users/*/friends
// @grant          GM_xmlhttpRequest
// @grant          GM_addStyle
// ==/UserScript==

FL_SPYSCOPE = {};
FL_SPYSCOPE.CONFIG = {
    'debug': false, // switch to true to debug.
};

// Utility debugging function.
FL_SPYSCOPE.log = function (msg) {
    if (!FL_SPYSCOPE.CONFIG.debug) { return; }
    GM_log('FETLIFE SPYSCOPE: ' + msg);
};

// Initializations.
var uw = (unsafeWindow) ? unsafeWindow : window ; // Help with Chrome compatibility?
GM_addStyle('\
.fl-spyscope {\
    border: 1px solid gray;\
    max-width: 500px;\
}\
.group_mods .fl-spyscope {\
    float: left;\
}\
.post .fl-spyscope,\
.pictures .fl-spyscope,\
.friends .fl-spyscope {\
    position: absolute;\
    background: black;\
}\
.post .last_comment .fl-spyscope {\
    position: static;\
}\
/* Events page and friends lists. */\
.pictures > li,\
.friends > li { position: relative; }\
.pictures .fl-spyscope,\
.friends .fl-spyscope { z-index: 100; }\
.pictures .fl-spyscope { top: 60px; }\
.friends .fl-spyscope { top: 40px; }\
.pictures .fl-spyscope li,\
#profile .friends .fl-spyscope li { width: 300px; }\
.pictures .fl-spyscope a { float: none; }\
#profile .friends .fl-spyscope a { display: inline; }\
.friends .fl-spyscope img { float: none; }\
');
FL_SPYSCOPE.users_cache = {};
FL_SPYSCOPE.init = function () {
    FL_SPYSCOPE.main();
};
window.addEventListener('DOMContentLoaded', FL_SPYSCOPE.init);

/**
 * Given a user ID, fetch and store the complete FetLife profile HTML for this user.
 *
 * @see https://userscripts.org/scripts/review/146293#function.FL_SPYSCOPE.getUserProfile
 */
FL_SPYSCOPE.fetchUserProfile = function (id) {
    if (FL_SPYSCOPE.users_cache[id].profile_html) {
        return; // we've already got a cache, so don't do this again.
    }
    FL_SPYSCOPE.users_cache[id] = {};
    GM_xmlhttpRequest({
        'method': 'GET',
        'url': 'https://fetlife.com/users/' + id.toString(),
        'onload': function (response) {
            // Get profile HTML.
            var html = response.responseText;
            // Store it in a local cache?
            FL_SPYSCOPE.users_cache[id].profile_html = html;

            // Parse the returned profile HTML and save relevant info.
            var parser = new DOMParser();
            var doc = parser.parseFromString(html, 'text/html');
            // NOTE: We collect the nickname in the FL_SPYSCOPE.main() function, since we can.
            FL_SPYSCOPE.users_cache[id].nickname = doc.querySelector('.bottom').childNodes[0].nodeValue;
            FL_SPYSCOPE.users_cache[id].sex      = FL_SPYSCOPE.getSex(doc.querySelector('.bottom'));
            FL_SPYSCOPE.users_cache[id].age      = FL_SPYSCOPE.getAge(doc.querySelector('.bottom'));
            FL_SPYSCOPE.users_cache[id].role     = FL_SPYSCOPE.getRole(doc.querySelector('.bottom'));
            FL_SPYSCOPE.users_cache[id].loc_str  = doc.querySelector('.bottom + p').innerHTML;
            FL_SPYSCOPE.users_cache[id].activity = doc.querySelector('#mini_feed');
        }
    });
};

/**
 * Various user info parsing functions.
 *
 * @see https://userscripts.org/scripts/review/146293#function.FL_SPYSCOPE.getSex
 */
FL_SPYSCOPE.getSex = function (el) {
    x = el.querySelector('.quiet').innerHTML;
    sex = x.match(/^\d\d(\S*)/);
    return sex[1];
};

FL_SPYSCOPE.getAge = function (el) {
    x = el.querySelector('.quiet').innerHTML;
    age = x.match(/^\d\d/);
    return parseInt(age);
};

FL_SPYSCOPE.getRole = function (el) {
    x = el.querySelector('.quiet').innerHTML;
    role = x.match(/ ?(\S+)?$/);
    return role[1];
};

FL_SPYSCOPE.createScope = function (id) {
    var div = document.createElement('div');
    div.setAttribute('class', 'fl-spyscope');

    var ul = document.createElement('ul');
    var li = document.createElement('li');
    
    var html_str = '';
    // Fill list items appropriately.
    
    html_str += '<b><font color="red">';
    html_str += FL_SPYSCOPE.users_cache[id].nickname;
    html_str += '</font> ';

    html_str += FL_SPYSCOPE.users_cache[id].age;
    html_str += FL_SPYSCOPE.users_cache[id].sex;
    html_str += ' ';
    html_str += FL_SPYSCOPE.users_cache[id].role + '</b><br />';
    html_str += '(' + FL_SPYSCOPE.users_cache[id].loc_str + ')';
    li.innerHTML = html_str;

    ul.appendChild(li);
    div.appendChild(ul);
    
    // Show last three items from "Latest activity" mini feed.
    //var acts = FL_SPYSCOPE.users_cache[id].activity.children;
    //div.innerHTML += '<ul><li>' + acts[0].innerHTML + '</li><li>' + acts[1].innerHTML + '</li><li>' + acts[2].innerHTML + '</li></ul>';

    return div;
};

/**
 * Handles spyscope rollovers.
 */
FL_SPYSCOPE.show = function (e) {
    scope = FL_SPYSCOPE.createScope(parseInt(e.currentTarget.href.match(/\d+$/)));
    e.currentTarget.parentNode.appendChild(scope);
};
FL_SPYSCOPE.hide = function (e) {
    var scope = e.currentTarget.parentNode.lastChild;
    scope.parentNode.removeChild(scope);
};

// This is the main() function, executed on page load.
FL_SPYSCOPE.main = function () {
    // Find all FetLife users on this page that aren't the current (logged-in) user.
    var user_links = document.querySelectorAll('a[href^="/users/"]:not([href^="/users/' + uw.FetLife.currentUser.id + '"])');
    // For each user,
    for (var i = 0; i < user_links.length; i++) {
        // Collect its user ID number.
        var id = parseInt(user_links[i].href.match(/(\d+)\/?$/));
        FL_SPYSCOPE.users_cache[id] = {};
        if (null !== id) {
            // Gather profile data for this user.

            // Get nickname.
            var n;
            if (user_links[i].children.length) {
                // This is an avatar link, not a text link.
                n = user_links[i].childNodes[0].alt;
                user_links[i].childNodes[0].title = "";
            } else {
                // This is a text link. Easy.
                n = user_links[i].innerHTML;
            }
            FL_SPYSCOPE.users_cache[id].nickname = n;

            // Collect age/sex/role/location.
            // TODO: Loading them all on page load really slows things down.
            //       Can we optimize so that we fetch on rollover rather than load?
            FL_SPYSCOPE.fetchUserProfile(id);
        }
        // Attach the spyscope show/hide event handler.
        user_links[i].addEventListener('mouseover', FL_SPYSCOPE.show);
        user_links[i].addEventListener('mouseout', FL_SPYSCOPE.hide);
    }
};

// The following is required for Chrome compatibility, as we need "text/html" parsing.
/*
 * DOMParser HTML extension
 * 2012-09-04
 *
 * By Eli Grey, http://eligrey.com
 * Public domain.
 * NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
 */

/*! @source https://gist.github.com/1129031 */
/*global document, DOMParser*/

(function(DOMParser) {
	"use strict";

	var
	  DOMParser_proto = DOMParser.prototype
	, real_parseFromString = DOMParser_proto.parseFromString
	;

	// Firefox/Opera/IE throw errors on unsupported types
	try {
		// WebKit returns null on unsupported types
		if ((new DOMParser).parseFromString("", "text/html")) {
			// text/html parsing is natively supported
			return;
		}
	} catch (ex) {}

	DOMParser_proto.parseFromString = function(markup, type) {
		if (/^\s*text\/html\s*(?:;|$)/i.test(type)) {
			var
			  doc = document.implementation.createHTMLDocument("")
			;

			doc.body.innerHTML = markup;
			return doc;
		} else {
			return real_parseFromString.apply(this, arguments);
		}
	};
}(DOMParser));