// twitterlib.js (c) 2011 Remy Sharp // @version 1.0.9 / Sun Feb 19 23:05:25 2012 +0000 // MIT license: http://rem.mit-license.org (function (global) { var twitterlib = {}; // for Node.js - a quasi document polyfill if (typeof exports !== 'undefined' && typeof require === 'function') { var urlparse = require('url').parse, http = require('http'); this.document = { location: { protocol: 'http:' }, head: { appendChild: function (element) { if (!element.src) return; // exit if we're not a script // send request var urldata = urlparse(element.src, true), request = http.request(urldata), json = ''; var callback = window[urldata.query.callback]; request.on('response', function (res) { res.setEncoding('utf8'); res.on('data', function(chunk) { json += chunk; }).on('end', function() { switch (res.statusCode) { case 200: json = json.replace(new RegExp('^' + urldata.query.callback + '\\('), '').replace(/\);*$/, ''); outstanding[urldata.query.callback] && callback(JSON.parse(json)); break; case 304: break; case 401: // console.error('not authed'); break; } }); }).end(); } }, getElementById: function () {}, createElement: function (type) { return { type: type, id: null, src: '' }; } }; } var guid = +new Date, window = this, document = window.document, head = document.head || document.getElementsByTagName('head')[0], last = {}, // memorisation object for the next method outstanding = {}, // reference object to allow us to cancel JSONP calls (though nulling the return function) ENTITIES = { '"': '"', '<': '<', '>': '>' }, protocol = document.location.protocol.substr(0, 4) === 'http' ? document.location.protocol : 'http:', URLS = { search: protocol + '//search.twitter.com/search.json?q=%search%&page=%page|1%&rpp=%limit|100%&since_id=%since|remove%&result_type=recent&include_entities=true', // TODO allow user to change result_type timeline: protocol + '//api.twitter.com/1/statuses/user_timeline.json?screen_name=%user%&count=%limit|200%&page=%page|1%&since_id=%since|remove%include_rts=%rts|false%&include_entities=true', list: protocol + '//api.twitter.com/1/%user%/lists/%list%/statuses.json?page=%page|1%&per_page=%limit|200%&since_id=%since|remove%&include_entities=true&include_rts=%rts|false%', favs: protocol + '//api.twitter.com/1/favorites/%user%.json?include_entities=true&skip_status=true&page=%page|1%&since_id=%since|remove%', retweets: protocol + '//api.twitter.com/1/statuses/retweeted_by_user.json?screen_name=%user%&include_entities=true&count=%limit|200%&since_id=%since|remove%&page=%page|1%' }, urls = URLS, // allows for resetting debugging undefined, caching = false; var ify = function() { return { entities: function (t) { return t.replace(/(&[a-z0-9]+;)/g, function (m) { return ENTITIES[m]; }); }, link: function(t) { return t.replace(/[a-z]+:\/\/([a-z0-9-_]+\.[a-z0-9-_:~\+#%&\?\/.=]+[^:\.,\)\s*$])/ig, function(m, link) { return '' + ((link.length > 36) ? link.substr(0, 35) + '…' : link) + ''; }); }, at: function(t) { return t.replace(/(^|[^\w]+)\@([a-zA-Z0-9_]{1,15}(\/[a-zA-Z0-9-_]+)*)/g, function(m, m1, m2) { return m1 + '@' + m2 + ''; }); }, hash: function(t) { return t.replace(/(^|[^&\w'"]+)\#([a-zA-Z0-9_^"^<]+)/g, function(m, m1, m2) { return m.substr(-1) === '"' || m.substr(-1) == '<' ? m : m1 + '#' + m2 + ''; }); }, clean: function(tweet) { return this.hash(this.at(this.link(tweet))); } }; }(); var expandLinks = function (tweet) { if (tweet === undefined) return ''; var text = tweet.text, i = 0; if (tweet.entities) { // replace urls with expanded urls and let the ify shorten the link if (tweet.entities.urls && tweet.entities.urls.length) { for (i = 0; i < tweet.entities.urls.length; i++) { if (tweet.entities.urls[i].expanded_url) text = text.replace(tweet.entities.urls[i].url, tweet.entities.urls[i].expanded_url); // /g ? } } // replace media with url to actual image (or thing?) if (tweet.entities.media && tweet.entities.media.length) { for (i = 0; i < tweet.entities.media.length; i++) { if (tweet.entities.media[i].media_url || tweet.entities.media[i].expanded_url) text = text.replace(tweet.entities.media[i].url, tweet.entities.media[i].media_url ? tweet.entities.media[i].media_url : tweet.entities.media[i].expanded_url); // /g ? } } } return text; }; var time = function () { var monthDict = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; return { time: function (date) { var hour = date.getHours(), min = date.getMinutes() + "", ampm = 'AM'; if (hour == 0) { hour = 12; } else if (hour == 12) { ampm = 'PM'; } else if (hour > 12) { hour -= 12; ampm = 'PM'; } if (min.length == 1) { min = '0' + min; } return hour + ':' + min + ' ' + ampm; }, date: function (date) { var mon = monthDict[date.getMonth()], day = date.getDate()+'', dayi = ~~(day), year = date.getFullYear(), thisyear = (new Date()).getFullYear(), th = 'th'; // anti-'th' - but don't do the 11th, 12th or 13th if ((dayi % 10) == 1 && day.substr(0, 1) != '1') { th = 'st'; } else if ((dayi % 10) == 2 && day.substr(0, 1) != '1') { th = 'nd'; } else if ((dayi % 10) == 3 && day.substr(0, 1) != '1') { th = 'rd'; } if (day.substr(0, 1) == '0') { day = day.substr(1); } return mon + ' ' + day + th + (thisyear != year ? ', ' + year : ''); }, shortdate: function (time_value) { var values = time_value.split(" "), parsed_date = Date.parse(values[1] + " " + values[2] + ", " + values[5] + " " + values[3]), date = new Date(parsed_date), mon = monthDict[date.getMonth()], day = date.getDate()+'', year = date.getFullYear(), thisyear = (new Date()).getFullYear(); if (thisyear === year) { return day + ' ' + mon; } else { return day + ' ' + mon + (year+'').substr(2, 2); } }, datetime: function (time_value) { var values = time_value.split(" "), date = new Date(Date.parse(values[1] + " " + values[2] + ", " + values[5] + " " + values[3])); return this.time(date) + ' ' + this.date(date); }, relative: function (time_value) { var values = time_value.split(" "), parsed_date = Date.parse(values[1] + " " + values[2] + ", " + values[5] + " " + values[3]), date = new Date(parsed_date), relative_to = (arguments.length > 1) ? arguments[1] : new Date(), delta = ~~((relative_to.getTime() - parsed_date) / 1000), r = ''; delta = delta + (relative_to.getTimezoneOffset() * 60); if (delta <= 1) { r = '1 second ago'; } else if (delta < 60) { r = delta + ' seconds ago'; } else if (delta < 120) { r = '1 minute ago'; } else if (delta < (45*60)) { r = (~~(delta / 60)) + ' minutes ago'; } else if (delta < (2*90*60)) { // 2* because sometimes read 1 hours ago r = '1 hour ago'; } else if (delta < (24*60*60)) { r = (~~(delta / 3600)) + ' hours ago'; } else { r = this.shortdate(time_value); } return r; } }; }(); var filter = (function () { return { match: function (tweet, search, includeHighlighted) { var i = 0, s = '', text = tweet.text.toLowerCase(), notonly = (!search['and'] || !search['and'].length) && (!search['or'] || !search['or'].length); if (typeof search == "string") { search = this.format(search); } // loop ignore first if (search['not'] && search['not'].length) { for (i = 0; i < search['not'].length; i++) { if (text.indexOf(search['not'][i]) !== -1) { return false; } } if (notonly) { return true; } } else if (({}).toString.call(search['not']) !== '[object Array]') { if (search['not'].test(text)) { return false; } if (notonly) { return true; } } if (search['and'] && search['and'].length) { for (i = 0; i < search['and'].length; i++) { s = search['and'][i]; if (s.substr(0, 3) === 'to:') { if (!RegExp('^@' + s.substr(3)).test(text)) { return false; } } else if (s.substr(0, 5) == 'from:') { if (tweet.user.screen_name !== s.substr(5)) { return false; } } else if (text.indexOf(s) === -1) { return false; } } } else if (typeof search['and'] == 'function') { if (search['and'].test(text)) { return true; } } if (search['or'] && search['or'].length) { for (i = 0; i < search['or'].length; i++) { s = search['or'][i]; if (s.substr(0, 3) === 'to:') { if (RegExp('^@' + s.substr(3)).test(text)) { return true; } } else if (s.substr(0, 5) == 'from:') { if (tweet.user.screen_name === s.substr(5)) { return true; } } else if (text.indexOf(search['or'][i]) !== -1) { return true; } } } else if (typeof search['or'] == 'function') { if (search['or'].test(text)) { return true; } } else if (search['and'] && search['and'].length) { return true; } return false; }, format: function (search, caseSensitive) { // search can match search.twitter.com format var blocks = [], ors = [], ands = [], i = 0, negative = [], since = '', until = ''; search.replace(/(-?["'](.*?)["']|\S+)/g, function (m) { // removed \b for chinese character support var neg = false; if (m.substr(0, 1) == '-') { neg = true; } m = m.replace(/["']+|["']+$/g, ''); if (neg) { negative.push(m.substr(1).toLowerCase()); } else { blocks.push(m); } }); for (i = 0; i < blocks.length; i++) { if (blocks[i] == 'OR' && blocks[i+1]) { ors.push(blocks[i-1].toLowerCase()); ors.push(blocks[i+1].toLowerCase()); i++; ands.pop(); // remove the and test from the last loop } else { ands.push(blocks[i].toLowerCase()); } } return { 'or' : ors, 'and' : ands, 'not' : negative }; }, // tweets typeof Array matchTweets: function (tweets, search, includeHighlighted) { var updated = [], tmp, i = 0; if (typeof search == 'string') { search = this.format(search); } for (i = 0; i < tweets.length; i++) { if (this.match(tweets[i], search, includeHighlighted)) { updated.push(tweets[i]); } } return updated; } }; })(); // based on twitter.com list of tweets, most common format for tweets function render(tweet) { var html = '