概要

動作デモ

ソース

TTLBrowser.html

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta http-equiv="Content-Script-Type" content="text/javascript">
<meta http-equiv="Content-Style-Type" content="text/css">
<link rel="stylesheet" type="text/css" href="http://www.takeash.net/take.css">
<title>Twitter TimeLine Browser</title>
</head>
<body>
<h1>Twitter TimeLine Browser</h1>
<form name="form1" action="#">
<div id="Control">
<table>
<tr>
	<th><button type="button" onclick="javascript:form1.tbSpecifiedId.value.match(/([^\/]+)(?:\/\w+\/(\d+))?$/); form1.tbUserName.value=RegExp.$1; browseTL(form1.tbUserName.value, RegExp.$2, form1.cbTruncate.checked);">Profile/StatusId</button></th>
	<td><input type="text" id="tbSpecifiedId" name="tbSpecifiedId" value="" size="70"></td>
</tr>
<tr>
	<th>UserName</th>
	<td><input id="tbUserName" name="tbUserName" type="text" value="" size="70"></td>
</tr>
<tr><td colspan="2" align="center">
	<button type="button" onclick="javascript:browseTL(form1.tbUserName.value, '', form1.cbTruncate.checked);">現在</button>
	<button type="button" onclick="javascript:browseTL(form1.tbUserName.value, form1.hdnAfterId.value, form1.cbTruncate.checked);">後の200件</button>
	<button type="button" onclick="javascript:browseTL(form1.tbUserName.value, form1.hdnAgoId.value, form1.cbTruncate.checked);">前の200件</button>
</td></tr>
<tr><td colspan="2" align="center">
	<button type="button" onclick="javascript:browseTL(form1.tbUserName.value, form1.hdnOneMonthAfterId.value, form1.cbTruncate.checked);">一ヶ月後</button>
	<button type="button" onclick="javascript:browseTL(form1.tbUserName.value, form1.hdnTwoWeeksAfterId.value, form1.cbTruncate.checked);">二週間後</button>
	<button type="button" onclick="javascript:browseTL(form1.tbUserName.value, form1.hdnOneWeekAfterId.value, form1.cbTruncate.checked);">一週間後</button>
	<button type="button" onclick="javascript:browseTL(form1.tbUserName.value, form1.hdnOneWeekAgoId.value, form1.cbTruncate.checked);">一週間前</button>
	<button type="button" onclick="javascript:browseTL(form1.tbUserName.value, form1.hdnTwoWeeksAgoId.value, form1.cbTruncate.checked);">二週間前</button>
	<button type="button" onclick="javascript:browseTL(form1.tbUserName.value, form1.hdnOneMonthAgoId.value, form1.cbTruncate.checked);">一ヶ月前</button>
</td></tr>
<tr><td colspan="2">
	<input type="checkbox" id="cbTruncate" name="cbTruncate" checked> <label for="cbTruncate">URLの表示を短縮</label>
</td></tr>
</table>
<input type="hidden" id="hdnAfterId" name="hdnAfterId" value="" size="30">
<input type="hidden" id="hdnAgoId" name="hdnAgoId" value="" size="30">
<input type="hidden" id="hdnOneMonthAfterId" name="hdnOneMonthAfterId" value="" size="30">
<input type="hidden" id="hdnTwoWeeksAfterId" name="hdnTwoWeeksAfterId" value="" size="30">
<input type="hidden" id="hdnOneWeekAfterId" name="hdnOneWeekAfterId" value="" size="30">
<input type="hidden" id="hdnOneWeekAgoId" name="hdnOneWeekAgoId" value="" size="30">
<input type="hidden" id="hdnTwoWeeksAgoId" name="hdnTwoWeeskAgoId" value="" size="30">
<input type="hidden" id="hdnOneMonthAgoId" name="hdnOneMonthAgoId" value="" size="30">
</div>
<div id="Result">
<p id="ResultUserName"></p>
<table id="ResultTable">
</table>
</div>
</form>
<div id="Caution">
<h2>注意</h2>
<ul>
<li>Twitter API を利用してタイムラインを表示します。</li>
<li>「Profile/StatusId」の横のテキストボックスに「<a href="https://twitter.com/#!/twj/status/90937652819398656">https://twitter.com/#!/twj/status/90937652819398656</a>」のようなリンクをペーストして同ボタンをクリックすると、そのツイート以前のツイートを表示します。</li>
<li>約200ツイート単位または一週間、二週間、一ヶ月単位で切替えられます。</li>
<li>前後はいずれも概算です。</li>
<li>古いツイートは表示されないようです。(max_idの制限?)</li>
<li>Opera, Firefox, Chrome で動作確認。</li>
<li>IE は table タグの innerHTML を書き換えられないようなので動きません。(<a href="http://support.microsoft.com/kb/239832/ja">仕様です</a>)</li>
<li>リンクは自前で判別せずに<a href="https://dev.twitter.com/docs/tweet-entities">Tweet Entities</a>を使うようにしてるけど、結構リンクにならないことが多いな。</li>
</ul>
</div>
<div id="Link">
<h2>Link</h2>
<ul>
<li><a href="http://www.takeash.net/wiki/?JavaScript/Twitter_TimeLineBrowser">Twitter TimeLine Browser</a></li>
<li><a href="https://github.com/remy/twitterlib">remy Twitter Lib</a></li>
<li><a href="http://code.google.com/p/twitterjs/">remy twitterjs</a></li>
<li><a href="http://watcher.moe-nifty.com/memo/2007/04/twitter_api.html">Twitter API 仕様書 (勝手に日本語訳シリーズ)</a></li>
<li><a href="https://dev.twitter.com/docs/api">REST API Resources</a></li>
<li><a href="https://dev.twitter.com/docs/api/1/get/statuses/user_timeline">GET statuses/user_timeline</a></li>
<li><a href="https://dev.twitter.com/docs/tweet-entities">Tweet Entities</a></li>
</ul>
</div>
<script type="text/javascript" src="twitterlib_tak.js"></script>
<script type="text/javascript" src="TTLBrowser.js"></script>
</body>
</html>

TTLBrowser.js

function browseTL(username, maxid, truncate){
	var twhost = 'https://twitter.com/#!/';
	var options = {};
	options.enableLinks = true;
	options.truncateLinks = truncate;
	// options.limit = 50;
	options.target_username = document.getElementById('ResultUserName');
	options.target_table = document.getElementById('ResultTable');
	options.template_username = '<a href="' + twhost + '%user_screen_name%" target="_blank">' 
		+ '<img  width="48" height="48" src="%user_profile_image_url%"></a> ' 
		+ '<a href="'+twhost+'%user_screen_name%" target="_blank">%user_name% ' 
		+ '(%user_screen_name%)</a>';
	options.template_head = '<tr><th>#</th><th>日時</th><th>本文</th><th>返信</th></tr>';
	options.template_table = '<tr><td align="center"><button type="button" '
		+ 'onclick="javascript:browseTL(form1.tbUserName.value, \'%id_str%\');">%index%</button></td>'
		+ '<td><a href="' + twhost + '%user_screen_name%/status/%id_str%" target="_blank">%time%</a></td>' 
		+ '<td>%text%</td><td align="center">%reply%</td>' 
		+ '</tr>';
	options.maxid = (maxid && maxid.match(/^\d+$/)) ? maxid : undefined ;
	options.tags = {};
	options.tags.afterid = document.getElementById('hdnAfterId');
	options.tags.agoid = document.getElementById('hdnAgoId');
	options.tags.onemonthafterid = document.getElementById('hdnOneMonthAfterId');
	options.tags.twoweeksafterid = document.getElementById('hdnTwoWeeksAfterId');
	options.tags.oneweekafterid = document.getElementById('hdnOneWeekAfterId');
	options.tags.oneweekagoid = document.getElementById('hdnOneWeekAgoId');
	options.tags.twoweeksagoid = document.getElementById('hdnTwoWeeksAgoId');
	options.tags.onemonthagoid = document.getElementById('hdnOneMonthAgoId');
	twitterlib_tak.timeline( username, options, function(tweets, options){
		var html = [];
		html.push( options.template_head );
		for (var i = 0; i < tweets.length; ++i){
			tweets[i].time = twitterlib_tak.time.localtime( tweets[i].created_at );
			tweets[i].reply = tweets[i].in_reply_to_status_id_str 
				? '<a href="' + twhost + tweets[i].in_reply_to_screen_name + '/status/' 
					+ tweets[i].in_reply_to_status_id_str + '" target="_blank">元</a> ' 
					+ '<button type="button" onclick="javascript:form1.tbUserName.value=\'' 
					+ tweets[i].in_reply_to_screen_name + '\'; browseTL(\'' 
					+ tweets[i].in_reply_to_screen_name + '\', \'' 
					+ tweets[i].in_reply_to_status_id_str + '\');">表示</button>'
				: '';
			tweets[i].index = i + 1;
			for (var key in tweets[i].user) {
				tweets[i]['user_' + key] = tweets[i].user[key];
			}
			if (options.template_table) {
				html.push( options.template_table.replace(/%([a-z_\-\.]*)%/ig, function (m, l){
					var r = tweets[i][l] + "" || "";
					if (l == 'text') {
						r = twitterlib_tak.expandLinks(tweets[i], options.truncateLinks);
						r = r.replace( /\n/g, "<br>\n" );
					}
					return r;
				}));
			} else {
				html.push(twitterlib_tak.render(tweets[i]));
			}
		}
		options.target_table.innerHTML = html.join('');
		if ( tweets.length > 0 ){
			options.target_username.innerHTML = 
				options.template_username.replace(/%([a-z_\-\.]*)%/ig, function (m, l){
					var r = tweets[0][l] + "" || "";
					if (l == 'text') r = twitterlib_tak.expandLinks(tweets[0], options.truncateLinks);
					return r;
				});
			if ( tweets.length > 1 ){
				var newest_id = parseInt( tweets[0].id_str );
				var oldest_id = parseInt( tweets[tweets.length - 1].id_str );
				var newest_sec = new Date( tweets[0].created_at ).getTime();
				var oldest_sec = new Date( tweets[tweets.length - 1].created_at ).getTime();
				var diff_id = newest_id - oldest_id;
				var postrate = diff_id / (( newest_sec - oldest_sec) / (24 * 3600 * 1000));
				options.tags.afterid.value = newest_id + diff_id;
				options.tags.agoid.value = oldest_id;
				options.tags.onemonthafterid.value = newest_id + postrate * 30;
				options.tags.twoweeksafterid.value = newest_id + postrate * 14;
				options.tags.oneweekafterid.value = newest_id + postrate * 7;
				options.tags.oneweekagoid.value = newest_id - postrate * 7;
				options.tags.twoweeksagoid.value = newest_id - postrate * 14;
				options.tags.onemonthagoid.value = newest_id - postrate * 30;
			}
		}
	});
}

twitterlib_tak.js

--- RemySharp\twitterlib.js	2011-10-31 10:31:23.444145000 +0900
+++ twitterlib_tak.js	2011-12-08 21:22:34.251240700 +0900
@@ -1,5 +1,7 @@
 // twitterlib.js (c) 2011 Remy Sharp
 // Licensed under the terms of the MIT license.
+// Original: https://github.com/remy/twitterlib
+// Modified by TakeAsh
 (function (twitterlib, container) {
   var guid = +new Date,
       window = this,
@@ -13,11 +15,11 @@
         '&gt;': '>'
       },
       URLS = {
-        search: 'http://search.twitter.com/search.json?q=%search%&page=%page|1%&rpp=%limit|100%&since_id=%since|remove%&result_type=recent', // TODO allow user to change result_type
-        timeline: 'http://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: 'http://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: 'http://api.twitter.com/1/favorites/%user%.json?page=%page|1%include_entities=true&skip_status=true',
-        retweets: 'http://api.twitter.com/1/statuses/retweeted_by_user.json?screen_name=%user%&count=%limit|200%&since_id=%since|remove%&page=%page|1%'
+        search: 'https://search.twitter.com/search.json?q=%search%&page=%page|1%&rpp=%limit|100%&since_id=%since|remove%&result_type=recent', // TODO allow user to change result_type
+        timeline: 'https://api.twitter.com/1/statuses/user_timeline.json?id=%user%&count=%limit|200%&page=%page|1%&since_id=%since|remove%include_rts=%rts|false%&max_id=%maxid|remove%&include_entities=true',
+        list: 'https://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: 'https://api.twitter.com/1/favorites/%user%.json?page=%page|1%include_entities=true&skip_status=true',
+        retweets: 'https://api.twitter.com/1/statuses/retweeted_by_user.json?id=%user%&count=%limit|200%&since_id=%since|remove%&page=%page|1%'
       },
       urls = URLS, // allows for resetting debugging
       undefined,
@@ -51,7 +53,7 @@
     };
   }();
   
-  var expandLinks = function (tweet) {
+  var expandLinks = function (tweet, truncateLinks) {
     if (tweet === undefined) return '';
     
     var text = tweet.text,
@@ -60,14 +62,48 @@
       // 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 ?
+          var link = tweet.entities.urls[i].expanded_url;
+          if ( link ) {
+            text = text.replace(tweet.entities.urls[i].url, '<a title="' + link + '" href="'+ link + '">'+((truncateLinks && link.length > 36) ? link.substr(0, 35) + '&hellip;' : link)+'</a>');
+          }
         }
       }
       
       // 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 ?
+          var imgurl = tweet.entities.media[i].media_url_https || tweet.entities.media[i].media_url || tweet.entities.media[i].expanded_url;
+          if ( imgurl ) {
+            text = text.replace(tweet.entities.media[i].url, '<a href="' + imgurl + '" title="' + imgurl + '" target="_blank">' + ((truncateLinks && imgurl.length > 36) ? imgurl.substr(0, 35) + '&hellip;' : imgurl) + '</a>' ); // /g ?
+            text += '\n<a href="' + imgurl + '" target="_blank"><img src="' + imgurl + '" width="' + tweet.entities.media[i].sizes.small.w + '" height="' + tweet.entities.media[i].sizes.small.h + '"></a>'
+          }
+        }
+      }
+      
+      // replace user mentions with user link
+      if ( tweet.entities.user_mentions && tweet.entities.user_mentions.length ) {
+        tweet.entities.user_mentions.sort( function( a, b ){ return ( a.screen_name.length < b.screen_name.length ) ? 1 : -1 ; } );
+        for (i = 0; i < tweet.entities.user_mentions.length; i++) {
+          var screen_name = tweet.entities.user_mentions[i].screen_name;
+          var re = new RegExp( '@'+screen_name, 'gi' );
+          if ( screen_name ) text = text.replace( re, '@<a href="https://twitter.com/' + screen_name + '">' + screen_name + '</a>');
+        }
+      }
+      
+      // replace hash tag with search link
+      if ( tweet.entities.hashtags && tweet.entities.hashtags.length ) {
+        //alert( tweet.entities.hashtags.length );
+        tweet.entities.hashtags.sort( function( a, b ){ return ( a.text.length < b.text.length ) ? 1 : -1 ; } );
+        for (i = 0; i < tweet.entities.hashtags.length; i++) {
+          var hashtext = tweet.entities.hashtags[i].text;
+          var re = new RegExp( "(#|#)" + hashtext, 'g' );
+          if ( hashtext ) {
+            //alert(hashtext);
+            if ( text.match( re ) ){
+              //alert(RegExp.$1 + "\n" + hashtext + "\n" + text);
+            }
+            text = text.replace( re, '$1<a href="https://search.twitter.com/search?q=%23' + encodeURIComponent(hashtext) + '">' + hashtext + '</a>');
+          }
         }
       }
 
@@ -170,7 +206,10 @@
         }
 
         return r;
-      }    
+      },
+      localtime: function(time_value){
+        return new Date( time_value ).toLocaleString();
+      }
     };
   }();
   
@@ -318,7 +357,7 @@
     html += tweet.user.screen_name + '" ';
     html += 'title="' + tweet.user.name + '">' + tweet.user.screen_name + '</a></strong> ';
     html += '<span class="entry-content">';
-    html += container[twitterlib].ify.clean(container[twitterlib].expandLinks(tweet));
+    html += container[twitterlib].expandLinks(tweet);
     html += '</span> <span class="meta entry-meta"><a href="http://twitter.com/' + tweet.user.screen_name;
     html += '/status/' + tweet.id_str + '" class="entry-date" rel="bookmark"><span class="published" title="';
     html += container[twitterlib].time.datetime(tweet.created_at) + '">' + container[twitterlib].time.relative(tweet.created_at) + '</span></a>';
@@ -590,4 +629,4 @@
   container[twitterlib].custom('timeline');
   container[twitterlib].custom('favs');
   container[twitterlib].custom('retweets');
-})('twitterlib', this);
+})('twitterlib_tak', this);

リンク