// ==UserScript== // @name Helgon.net avatars in user listings // @namespace http://www.lysator.liu.se/~jhs/userscript // @description Show avatars next to user names on Helgon.net user listings. // @include http://*helgon.net/*.asp* // ==/UserScript== // every sex listed in here with a true value will get avatars shown in lists: var show = { T:true, O:true, K:true }; var poll = 7 * 24 * 60 * 60 * 1000; // refetch image URLs weekly var purl = '/userinfo/userinfo.asp?ID='; // profile URL link var host = 'http://' + location.host; // XPath expression to match all profile links we want to link images to var profile_links = '//a[starts-with(@href,"'+ purl +'")]' + sexpath( show ); // regexps for detecting profile URLs and for parsing profile content data var id_re = new RegExp( ']* src=.?http://g\\.helgon\\.net/u/.{6,10}' + '([0-9A-F-]{36}).{1,3}\\.jpg', 'i' ); // (image uid) var tags = '(?:<[^>]*>)*'; var headline_re = new RegExp( ' ?'+ tags + '([^\\s<]*)'+ tags +' (.)(\\d+)'+ tags + '([^<]*) i ([^<]*)', 'i' ); // (name, sex, age, city, region) // extend the profile-link xpath to become gender specific (based on `show') function sexpath( show ) { var on = 0, sex, y, n; for( sex in show ) if( !show[sex] ) n = sex; else { on++; y = sex; } switch( on ) { case 0: return '[0=1]'; case 1: return '[starts-with(following-sibling::text()," '+ y +'")]'; case 2: return '[not(starts-with(following-sibling::text()," '+ n +'"))]'; case 3: return ''; } } // generic support functions: // Returns a function that calls `f' in the context of `self'. `f' gets called // with `args' concatenated to its arguments array, when the returned function // is called. Fuzzy? A really spaced-out "hello world" example: // make_caller( function( h, w ){ alert( h+this.s+w ); }, ['World!'], {s:' '} ) // ( 'Hello' ); function make_caller( f, args, self ) { return function() { args = Array.prototype.slice.call( arguments ).concat( args||[] ); f.apply( self||f, args ); }; } // Fetch a web page and call cb( http_response, args[0], ..., args[N] ) function get( url, cb, args ) { //if( args ) cb = make_caller( cb, args ); GM_xmlhttpRequest( { method:'GET', url:url, onload:cb } ); } // list nodes matching this expression, optionally relative to the node `root' function $x( xpath, root ) { var doc = root ? root.evaluate ? root : root.ownerDocument : document; var got = doc.evaluate( xpath, root||doc, null, 0, null ), next, result = []; while( next = got.iterateNext() ) result.push( next ); return result; } // run the passed cb( node, index ) on all nodes matching the expression function foreach( xpath, cb ) { var nodes = $x( xpath ), i, e = 0; for( i=0; i Singel< function User( id, data ) { if( typeof data == 'object' ) this.init( id, data.u, data.s, data.a, data.n, data.c, data.r, data.t ); } User.prototype.init = function( id, uid, sex, age, name, city, region, t ) { this.id = id; this.uid = uid; this.sex = sex; this.age = age; this.name = name; this.city = city; this.region = region; this.timestamp = t; }; User.prototype.get_image_url = function() { var prefix = this.uid.substring( 0, 3 ); return 'http://g.helgon.net/u/%7B'+ prefix +'/%7B'+ this.uid +'%7D.jpg'; }; User.prototype.encode = function() { var n = { name:'n', sex:'s', age:'a', /*city:'c', region:'r',*/ uid:'u', timestamp:'t' }, u = {}, property; for( property in this ) if( n[property] ) u[n[property]] = this[property]; return u; }; // run cb( args[0], ... args[N] ) with the this object set to a given user (id, // url or User object), fetching it as needed from the server or our user cache User.apply = function( user, cb, args ) { var id; if( typeof user == 'object' ) // we got a User object return cb.apply( user, args ); if( typeof user == 'string' ) // we got a url (or id) id = Users.id_from_url( user ); if( typeof user == 'number' ) // we got a user id id = user; if( user = Users.get( id ) ) // we had a cached copy { cb.apply( user, args ); if( (new Date).getTime() - user.timestamp < poll ) return; // ...and there was no need to refresh it cb = 0; // refresh, but don't add the avatar again. } // okay, we need to pull the user data from the server var url = host + purl + id; // this generates a call User.parse( xhrdata, url, cb, args ) call: get( url, make_caller( User.parse, [url, cb, args] ) ); }; // Parse profile data, update the user cache, and optionally call the provided // callback with the this object set to the appropriate User, passing on args. User.parse = function( data, url, cb, args ) { var u = new User, html = typeof data=='string' ? data : data.responseText; html = html.replace( /\s+/gm, ' ' ); u.id = Users.id_from_url( url ); if( !u.id ) return; // probably our own profile -- just ignore it, for now u.uid = id_re.exec( html )[1]; if( data = headline_re.exec( html ) ) // name, sex, age, city, region { u.name = data[1]; u.sex = data[2]; u.age = parseInt( data[3], 10 ); u.city = data[4]; u.region = data[5]; } u.timestamp = (new Date).getTime(); Users.save( u ); if( cb ) cb.apply( u, args||[] ); }; // avatar script function inject_avatar_before( node ) // assumes a User this object { // exit early to cop out of showing the ugly (drawn) default avatars if( this.uid.match( /00000000-0000-0000-0000-0000000000/ ) ) return; node.parentNode.style.minHeight = '60px'; var img = document.createElement( 'img' ); with( img ) { src = this.get_image_url(); className = 'largeimageborder'; style.width = '40px'; style.height = '56px'; align = 'middle'; } var a = document.createElement( 'a' ); a.href = host + purl + this.id; a.appendChild( img ); var space = document.createTextNode(' '); node.parentNode.insertBefore( space, node ); node.parentNode.insertBefore( a, space ); } function fetch_and_inject_avatar( node, n ) { User.apply( node.href, inject_avatar_before, [node] ); } switch( location.pathname.toLowerCase() ) { case '/': case '/start/start.asp': case '/guestbook/guestbook.asp': break; case '/userinfo/userinfo.asp': // just update the cache for this user User.parse( document.documentElement.innerHTML, location.href ); break; default: foreach( profile_links, fetch_and_inject_avatar ); }