Some more twitter stuff
[social_wok.user.js] / social_wok.user.js
1 // ==UserScript==
2 // @name        Social commentaries for the Wok
3 // @description Add likes, comments and references from social sites to the Wok pages
4 // @namespace   http://oblomov.myopenid.com
5 // @include     http://wok.oblomov.eu/*
6 // @include     http://localhost/~oblomov/wok/*
7 // @author      Giuseppe "Oblomov" Bilotta
8 // @version     20110123
9 // ==/UserScript==
10
11
12 if (typeof(unsafeWindow) == 'undefined') unsafeWindow = window;
13
14 var social = function() {
15         var img_preload = new Image();
16         var now = new Date();
17         var doc = unsafeWindow.document;
18
19         /* list of the permalinks we want to check social networks for.
20            the first one will always be the document location */
21         this.permalink = [ unsafeWindow.location.toString() ];
22
23         /* parents to the permalink */
24         this.permalink_parent = [ doc.getElementById('pageinfo') ];
25
26         var load_script = function(url) {
27                 var e = doc.createElement('script');
28                 e.type = 'text/javascript';
29                 e.src = url;
30                 e.async = true;
31                 doc.getElementsByTagName('head')[0].appendChild(e);
32         };
33
34         var weekday = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
35         var month = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
36
37         var date2link = function(d, l) {
38                 var cal;
39                 var data;
40
41                 if (typeof(d.getDate) == 'function') {
42                         cal = d;
43                 } else {
44                         cal = new Date();
45
46                         cal.setUTCFullYear(parseInt(d.substring(0,4)));
47                         cal.setUTCMonth(parseInt(d.substring(5,7)) - 1);
48                         cal.setUTCDate(parseInt(d.substring(8,10)));
49                         cal.setUTCHours(parseInt(d.substring(11,13)));
50                         cal.setUTCMinutes(parseInt(d.substring(14,16)));
51                         cal.setUTCSeconds(parseInt(d.substring(17,19)));
52                 }
53
54                 var delta = (now - cal)/1000;
55
56                 if (delta < 48*3600) {
57                         if (cal.getDate() == now.getDate()) {
58                                 data = 'today';
59                         } else {
60                                 data = 'yesterday';
61                         }
62                 } else if (cal.getYear() == now.getYear()) {
63                         data = 'on ' + weekday[cal.getDay()] + ', ' + month[cal.getMonth()] + ' ' + cal.getDate(); 
64                 } else {
65                         data = 'on ' + [cal.getYear(), cal.getMonth() + 1, cal.getDate()].join('-');
66                 }
67
68                 data += ' at ' + [cal.getHours(), cal.getMinutes()].join(':');
69
70                 if (l) {
71                         return '<a href="' + l + '">' + data + '</a>';
72                 } else {
73                         return data;
74                 }
75         };
76
77         var create_style = function(string) {
78                 var ssheet = doc.createElement('style');
79                 ssheet.type = 'text/css';
80                 ssheet.innerHTML = string;
81                 doc.getElementsByTagName('head')[0].appendChild(ssheet);
82         };
83
84         this.gen_style = function() {
85                 var cssrules = '.social ';
86                 cssrules += '{display:block;border:2px inset black;height:auto;padding:0;font-size:90%;line-height:1;margin:0}';
87                 cssrules += '.invisible {display:none}';
88                 cssrules += '.social.logo {padding:0;margin:0 auto;display:inline-block;border:2px lightblue}';
89                 cssrules += '.social.logo.inactive {border-style:outset}';
90                 cssrules += '.social.logo.active {border-style:inset}';
91                 create_style(cssrules);
92         };
93
94         this.friendfeed = new function() {
95                 img_preload.src = 'http://friendfeed.com/favicon.ico';
96                 img_preload.src = 'http://friendfeed.com/static/images/logo-small.png';
97
98                 var from2link = function(from) {
99                         return '<a href="http://friendfeed.com/' + from.id + '">' + from.name + '</a>';
100                 };
101
102                 var from2linkimg = function(from) {
103                         return '<a class="floatlink" href="http://friendfeed.com/' + from.id + '"><img src="http://friendfeed-api.com/v2/picture/' + from.id + '?size=small" /></a>';
104                 };
105
106                 this.gen_style = function() {
107                         var cssrules = '.friendfeed {border-color:lightblue}';
108                         cssrules += '.friendfeed .floatlink {margin:0 1ex .5ex 0;float:left}';
109                         cssrules += '.friendfeed .entrybody {margin:0;clear:both}';
110                         cssrules += '.friendfeed .entrydiv {padding:.5ex;border-top:.25ex dashed lightblue;text-align:justify}';
111                         cssrules += '.friendfeed .entrydiv:first-child {border-top:none}';
112                         cssrules += '.friendfeed .entryfrom {margin:.5ex 1ex .5ex 0;clear:both}';
113                         cssrules += '.friendfeed .entrycomments {margin-left:2.2em}';
114                         cssrules += '.friendfeed .entryliked {margin:0 2ex;text-indent:-2ex}';
115                         cssrules += '.friendfeed .entrycomment {border-top: .1ex dotted blue; padding:1ex .5ex}';
116                         cssrules += '.friendfeed .entrycomment p {margin:0}';
117                         cssrules += '.friendfeed .entrycommentfrom {font-size:smaller;line-height:150%}';
118                         create_style(cssrules);
119                 };
120
121                 this.process = function(linkidx, data) {
122                         var url = unsafeWindow.social.permalink[linkidx];
123                         if (!url)
124                                 return;
125                         if (!(data.entries && data.entries.length > 0))
126                                 return;
127
128                         var last_body = '';
129
130                         var ecount = data.entries.length;
131                         /* check if the only entry is from me and it has no comment --skip it then */
132                         var entry = data.entries[0];
133                         if (entry.from.id == 'oblomov' && !(entry.comments) && !(entry.likes))
134                                 return;
135
136                         var divid = 'friendfeed_'+linkidx;
137                         var pdiv = doc.getElementById(divid);
138
139                         if (!pdiv) {
140                                 pdiv = doc.createElement('div');
141                                 pdiv.setAttribute('class', 'social friendfeed invisible');
142                                 pdiv.setAttribute('id', divid);
143                                 pdiv.setAttribute('title', 'FriendFeed comments on '+url);
144
145                                 unsafeWindow.social.permalink_parent[linkidx].appendChild(pdiv);
146
147                                 var logo = doc.createElement('img');
148                                 logo.className = 'social logo inactive';
149                                 logo.src = linkidx ? 'http://friendfeed.com/favicon.ico' : 'http://friendfeed.com/static/images/logo-small.png';
150                                 logo.setAttribute('alt', 'FriendFeed logo');
151                                 logo.setAttribute('title', 'FriendFeed logo');
152                                 pdiv.parentNode.insertBefore(logo,pdiv);
153
154                                 logo.addEventListener('click', function() {
155                                         if (pdiv.className.match(' invisible')) {
156                                                 logo.className = logo.className.replace(' inactive',' active');
157                                                 pdiv.className = pdiv.className.replace(' invisible','');
158                                         } else {
159                                                 logo.className = logo.className.replace(' active',' inactive');
160                                                 pdiv.className = pdiv.className + ' invisible';
161                                         }
162                                 }, false);
163                         }
164
165                         for (var i=0; i < ecount; ++i) {
166                                 var entry = data.entries[i];
167
168                                 var p_entry_id = divid + '_entry_' + entry.id;
169
170                                 // skip this entry if we already added it from a different source
171                                 if (doc.getElementById(p_entry_id))
172                                         continue;
173
174
175                                 var fed = '';
176                                 var ediv;
177                                 if (entry.body != last_body) {
178                                         last_body = entry.body;
179                                         fed += '<p class="entrybody">' + entry.body;
180
181                                         ediv = doc.createElement('div');
182                                         ediv.className = 'entrydiv';
183                                         pdiv.appendChild(ediv);
184                                 } else {
185                                         ediv = pdiv.lastChild;
186                                         fed = ediv.innerHTML;
187                                 }
188                                 fed += '<p id="' + p_entry_id + '" class="entryfrom">' + from2linkimg(entry.from) + 'By ' + from2link(entry.from) + '<br/>' + date2link(entry.date, entry.url);
189                                 fed += '<div class="entrycomments"></div>';
190                                 ediv.innerHTML = fed;
191                                 ediv = ediv.lastChild;
192
193                                 var likes = entry.likes || [];
194                                 var jcount = likes.length;
195                                 if (jcount > 0) {
196                                         var liked = '<p class="entryliked">Liked by ';
197                                         for (var j=0; j < jcount; ++j) {
198                                                 var like = likes[j];
199                                                 if (like.from)
200                                                         liked += (j > 0 ? ', ' : '') + from2link(like.from);
201                                                 else
202                                                         liked += ' and ' + like.body;
203                                         }
204
205                                         var ldiv = doc.createElement('div');
206                                         ldiv.innerHTML = liked;
207                                         ediv.appendChild(ldiv);
208                                 }
209
210                                 var comments = entry.comments || [];
211                                 jcount = comments.length;
212                                 if (jcount > 0) {
213                                         for (var j=0 ; j < jcount; ++j) {
214                                                 var comment = comments[j];
215                                                 var cdiv = doc.createElement('div');
216                                                 cdiv.className = 'entrycomment';
217                                                 ediv.appendChild(cdiv);
218
219                                                 if (comment.from) {
220                                                         cdiv.innerHTML = from2linkimg(comment.from) +
221                                                                 '<p class="entrycommentfrom">' + date2link(comment.date) + ' by ' + from2link(comment.from) +
222                                                                 '<p>' + comment.body;
223                                                 } else {
224                                                         cdiv.innerHTML += comment.body;
225                                                 }
226                                         }
227                                 }
228                         }
229                 };
230
231                 this.load = function(linkidx) {
232                         var url = unsafeWindow.social.permalink[linkidx];
233                         var callback = '(function(data){social.friendfeed.process('+linkidx+',data)})';
234                         var cb_enc = escape(callback);
235
236                         var wokloc = url.replace(/localhost\/~oblomov\/wok/g,'wok.oblomov.eu');
237                         /* the same page has at least two possible URLs: if it ends with /, it could be present without /,
238                            and if it ends with /?index.html it could be present without it */
239                         var locs = [wokloc];
240                         if (wokloc.charAt(wokloc.length-1) == '/') {
241                                 locs.push(wokloc.substring(0,wokloc.length-1));
242                         }
243                         if (wokloc.match(/\/index.html$/)) {
244                                 locs.push(wokloc.substring(0,wokloc.length-10));
245                                 locs.push(wokloc.substring(0,wokloc.length-11));
246                         }
247
248                         var locs_enc = [];
249                         for (var i=0; i < locs.length; ++i) {
250                                 locs_enc.push(encodeURI(locs[i]));
251                         }
252                         var ff_url = 'http://friendfeed-api.com/v2/url?url=' + locs_enc.join(',') +
253                                 '&callback=' + cb_enc + '&cache=' + Math.floor(Math.random()*1024*1024);
254                         load_script(ff_url);
255
256                         ff_url = 'http://friendfeed-api.com/v2/search?q=' + locs_enc[0] +
257                                 '&callback=' + cb_enc + '&cache=' + Math.floor(Math.random()*1024*1024);
258                         load_script(ff_url);
259                 };
260         };
261
262         this.twitter = new function() {
263                 img_preload.src = 'http://twitter.com/favicon.ico';
264
265                 this.gen_style = function() {
266                         var cssrules = '.twitter {border-color:lightblue;min-height:56px}';
267                         cssrules += '.twitter .floatlink {margin:0 1ex .5ex 0;float:left}';
268                         cssrules += '.twitter .entry {margin:0;clear:both;padding:.5ex;border-top:.25ex dashed lightblue;text-align:justify}';
269                         cssrules += '.twitter .entry:first-child {border-top:none}';
270                         cssrules += '.twitter .entryfrom {margin:.5ex 1ex .5ex 0}';
271                         create_style(cssrules);
272                 };
273
274                 this.process = function(linkidx, data) {
275                         var url = unsafeWindow.social.permalink[linkidx];
276                         if (!url)
277                                 return;
278                         if (!(data.results && data.results.length > 0))
279                                 return;
280
281                         var divid = 'twitter_'+linkidx;
282                         var pdiv = doc.getElementById(divid);
283
284                         if (!pdiv) {
285                                 pdiv = doc.createElement('div');
286                                 pdiv.setAttribute('class', 'social twitter invisible');
287                                 pdiv.setAttribute('id', divid);
288                                 pdiv.setAttribute('title', 'Twits on '+url);
289
290                                 unsafeWindow.social.permalink_parent[linkidx].appendChild(pdiv);
291
292                                 var logo = doc.createElement('img');
293                                 logo.className = 'social logo inactive';
294                                 logo.src = 'http://twitter.com/favicon.ico';
295                                 logo.setAttribute('alt', 'Twitter logo');
296                                 logo.setAttribute('title', 'Twitter logo');
297                                 pdiv.parentNode.insertBefore(logo,pdiv);
298
299                                 logo.addEventListener('click', function() {
300                                         if (pdiv.className.match(' invisible')) {
301                                                 logo.className = logo.className.replace(' inactive',' active');
302                                                 pdiv.className = pdiv.className.replace(' invisible','');
303                                         } else {
304                                                 logo.className = logo.className.replace(' active',' inactive');
305                                                 pdiv.className = pdiv.className + ' invisible';
306                                         }
307                                 }, false);
308                         }
309
310                         var ecount = data.results.length;
311                         for (var i=0; i < ecount; ++i) {
312                                 var entry = data.results[i];
313
314                                 var p_entry_id = divid + '_entry_' + entry.id;
315
316                                 // skip this entry if we already added it from a different source
317                                 if (doc.getElementById(p_entry_id))
318                                         continue;
319
320                                 var ediv = doc.createElement('div');
321                                 ediv.className = 'entrydiv';
322                                 pdiv.appendChild(ediv);
323
324                                 var profilink = 'http://twitter.com/' + entry.from_user;
325                                 var content = '<p class="entry"><a class="floatlink" href="' + profilink + '">';
326                                 content += '<img src="' + entry.profile_image_url + '"></a>';
327                                 content += entry.text + '</p>';
328                                 content += '<p class="entryfrom">By <a href="' + profilink + '">'+ entry.from_user + '</a> ';
329                                 var entrydate = new Date(entry.created_at);
330                                 content += date2link(entrydate, profilink + '/status/' + entry.id_str);
331
332                                 ediv.innerHTML = content;
333                         }
334                 }
335
336                 this.load = function(linkidx) {
337                         var url = unsafeWindow.social.permalink[linkidx];
338                         unsafeWindow.social.twitter['process_' + linkidx] = function(data) {
339                                 unsafeWindow.social.twitter.process(linkidx, data);
340                         }
341                         var callback = 'window.social.twitter.process_' + linkidx;
342                         var cb_enc = escape(callback);
343
344                         var wokloc = url.replace(/localhost\/~oblomov\/wok/g,'wok.oblomov.eu');
345                         /* the same page has at least two possible URLs: if it ends with /, it could be present without /,
346                            and if it ends with /?index.html it could be present without it */
347                         var locs = [wokloc];
348
349                         var locs_enc = [];
350                         for (var i=0; i < locs.length; ++i) {
351                                 locs_enc.push(encodeURI(locs[i]));
352                         }
353                         var twurl = 'http://search.twitter.com/search.json?q=' + locs_enc.join('+OR+') +
354                                 '&callback=' + cb_enc + '&cache=' + Math.floor(Math.random()*1024*1024);
355                         load_script(twurl);
356                 }
357         };
358
359         this.gen_style();
360         this.friendfeed.gen_style();
361         this.twitter.gen_style();
362
363         this.collect = function() {
364                 /* we don't need stuff like //div[contains(concat(' ',normalize-space(@class),' '),' foo ')]
365                    because we know they are single-classed */
366                 var footers = doc.evaluate('//div[@class="inlinefooter"]',doc,null,XPathResult.ORDERED_NODE_ITERATOR_TYPE,null);
367
368                 try {
369                         var footer = footers.iterateNext();
370                         while (footer) {
371                                 var perma = doc.evaluate('*//span[@class="permalink"]/a',footer,null,XPathResult.FIRST_ORDERED_NODE_TYPE,null).singleNodeValue;
372                                 if (perma) {
373                                         unsafeWindow.social.permalink.push(perma.href.toString());
374                                         unsafeWindow.social.permalink_parent.push(footer);
375                                 }
376                                 footer = footers.iterateNext();
377                         }
378                 } catch (e) {
379                         if (opera && opera.postError) {
380                                 opera.postError("failed to iterate over permalinks: " + e);
381                         }
382                 }
383         };
384
385         this.load = function() {
386                 for (var i=0; i < this.permalink.length; ++i) {
387                         unsafeWindow.social.friendfeed.load(i);
388                         unsafeWindow.social.twitter.load(i);
389                 }
390         }
391 };
392
393 unsafeWindow.social = new social();
394 unsafeWindow.social.collect();
395 unsafeWindow.social.load();
396