Wednesday, April 2, 2008

Playing with Javascript or what binds Greasemonkey, Twitter and Ambient Avatars together

It's been a while since I tried JavaScript hacking (almost 2 years). This time I had the haunting idea to create a Greasemonkey mashup so I can see my twitter page with the avatar next to each tweet exactly as it looked at the time the tweet was posted.

To do this the avatar history must be stored somewhere. That's where chinposin.com comes in. Initially originated as a refreshing avatar on Friday, it evolved into the Ambient Avatar Platform (TM) (credit goes to @monkchips and @yellowpark- you're great). In simple words- you follow @chinposin on twitter, and when you change your avatar, the old one is saved. So you have a gallery of all of your previous avatars for your previewing pleasure and along with the dates they were changed.

For those of you wondering what's twitter, that's a topic for an entire new blog post... or a whole blog, so start at wikipedia, so we can continue with the interesting stuff, shall we?

So there we are- we want to include info from one site into another- a task where Greasemonkey excels (normally JavaScript cannot just fetch info from any other site at whim).

I've obviously lost some of my JavaScript knowledge since it took me an obscene amount of time to get this tiny piece of code working. To start off, I had forgotten that Greasemonkey had also some restrictions, not only enhancements. For security purposes, a lot of objects were wrapped in XPCNativeWrapper and I had to use loads of wrappedJSObject as a workaround. Yes, I know it's not secure, and you should know this too.

Another issue I had a problem with was passing an argument to a closure. I eventually remembered that the closure is an object and you can just assign any field to an object, because each object is also an associative array. Accessing the function object from itself also took some googling- arguments.callee did the trick.

So is there anything that can be improved in this shoddy script? You bet. For starters, it loads the chinposin site a lot, sending 20 simultaneous requests right off the bat, even for duplicate user pages. I could cache the avatar history, but that would require that I synchronize the requests. This script could be modified into a Firefox extension, which has less restrictions than Greasemonkey. And I really should use a prototype for those twenty closures I create, but I gotta have something to do for next time, right?

Without further ado, here's the script. Copy it and paste it into twitteravatarhistory.user.js (OK, you can come up with a longer name if you're so inclined). Then open it with Firefox and if Greasemonkey is installed you will be presented with a dialog prompting you to install it. It's tested with Firefox 2.0.0.13, 3a9, 3b4 and Greasemonkey 0.6.6.20061017 and 0.7.20080121.0. Considering the rate of change, I would be surprised it works in 1 year.

// ==UserScript==
// @name TwitterAvatarHistory
// @description Shows tweets with the avatar at time of posting
// @include http://twitter.com/*
// ==/UserScript==

// Assumptions:
// -chinposin.com has a special date string under the pic
// -avatars are listed chronologically
// -many others regarding DOM position

const avatar_home = "http://www.chinposin.com/home/";
var twitter_images = document.evaluate('//.[contains(@class, "hentry")]', document, null, XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null);
while (message = twitter_images.iterateNext()) {
message = message.wrappedJSObject;

// Read user name
var url = message.getElementsByClassName("url")[0];
if (!url) continue;
var username = url.getAttribute("href").match("[^/]*$");

// Read date of message and extract fields with a regexp
var date_string = message.getElementsByClassName("published")[0].getAttribute("title");
var match = date_string.match(/(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})\+(\d{2}):(\d{2})/);
var date = new Date(match[1], match[2], match[3], match[4], match[5], match[6]);

var http = function(responseDetails) {
// add dummy element so we can operate on its DOM
var elem = document.createElement("html");
document.body.appendChild(elem);
elem.innerHTML = responseDetails.responseText;

// getElementById is only found in document object, will use XPath
var gallery = document.evaluate('//.[@id="gallery"]', elem.wrappedJSObject, null,
XPathResult.ANY_UNORDERED_NODE_TYPE, null).singleNodeValue.wrappedJSObject;

// Might be better to couple these more tightly than creating two separate arrays
var images = gallery.getElementsByTagName("img");
var dates = gallery.getElementsByClassName("mainText");

// Find avatar date not more recent than message date
for (i = 0; i < dates.length; i++) {
var match = dates[i].textContent.match(/(\d{4})-(\d{2})-(\d{2}) +(\d{2}):(\d{2}):(\d{2})/);
var avatar_date = new Date(match[1], match[2], match[3], match[4], match[5], match[6]);

if (avatar_date < arguments.callee.date) {
// Replace message pic with avatar corresponding to date
arguments.callee.img.firstChild.setAttribute("src", images[i].getAttribute("src"))
// TMTOWTDI:
//~ arguments.callee.img.replaceChild(images[i].cloneNode(false), arguments.callee.img.firstChild);
break;
}
}

// clean up temp structure
document.body.removeChild(elem);

}

// Trick to pass data to the closure
http.date = date;
http.img = message.getElementsByClassName("url")[0];

// Reach list of pix from user page
GM_xmlhttpRequest({method : "GET", url : avatar_home + username, onload : http});

}


Update: code formatting had munched some of the Greasemonkey header, that should be fixed now.

Update 10 April 2008: New code's on Greasemonkey repository since last week, today a fix was issued that adapts to twitter interface changes.

No comments: