ブログにTwitterの自分の発言を表示する

投稿: 2009年5月11日

ここのところ技術メモばかりで興味のない方には申し訳ないけれど、まぁ適当に読み飛ばしてください。しかも今回のは知らないうちに結構長くなってしまいました。

先日からこのブログのシステム更新をして、それからテンプレートを調整したりして更新前の状態に近づける努力をしているわけだけど、その一環としてTwitterの表示も復活させた。それに当たって、前々から感じていた問題点を解決すべく、ちょっと工夫してみた。前々から感じていた問題というのは、具体的には以下のようなもの:

  • JavaScriptを使いたくない
  • 他人への返信は表示したくない
  • TinyURLは展開して表示したい
  • URLをクリックできるようにしたい

とりあえず、久々にPerlスクリプトを書いてこれらの問題を解決した。

JavaScriptを使いたくない

自分の発言をブログなどに貼り付けたい時に使えるコードとしてTwitterから配布されているのは、JavaScriptを用いたものとFlashを用いたものである。そのうちFlash版はサンプルを見る限り全くアクセシブルには作られていないので論外として、これまではJavaScript版に手を入れて使ってきた。どう手を入れていたかというと、配布されているものでは1度に数個 (たぶん5個) の発言が表示されるようになっていたのを、1個だけ表示するようにしていた。 (手を入れるといっても、配布されているコードではTwitterのサーバ上にあるコードをクライアントにダウンロードさせて実行させる方式になっているので、まずはこのコードを自分の所に持ってきて手を入れて、そしてそれをダウンロードさせるようにする、という少々面倒な方法だ。) が、そもそもJavaScriptには以下のような問題があると感じていた。

  • JavaScriptを使わない/使えないブラウザでは表示されない
  • ブラウザとTwitterのサーバの通信が発生するので、その部分が遅いとページ全体の表示が引っ張られる可能性がある

前者については、だんだんそういう環境も減ってきたのでそれほど問題視する必要はないのでは、という意見もあることだろう。が、後者については、普通に表示している分には問題になりづらいかもしれないが、スクリーン・リーダーを使っている場合、表示が全て終わらないと他の部分の読み上げも正しくできない場合があるので、結構切実な問題である。

ということで、クライアント側で処理させて表示させるのではなく、サーバ側で処理して表示内容をページに埋め込んだ上でクライアントに送るという形にする必要がある。そこで、タイムラインを取得して、それを処理して表示に適した形にして出力するスクリプトをPerlで書いて、これをSSIを使って実行して、その結果をページに埋め込む方法を採用することにした。

他人への返信を表示したくない

前述のJavaScriptを使った表示の場合、とにかく自分の最新の発言が表示されてしまう。言うまでもなく、「@他人のユーザ名」で始まる他人への返信であっても表示されてしまう。一見どうでもいいことのような気もするが、以下のような問題を感じていた。

  • ブログを見に来てくれてTwitterを見ていない人にとっては、他人へのメッセージよりも僕が不特定多数に発した言葉の方が面白いのではないか -- 他人への返信は、その元となっている発言まで含めて読まないと文脈が分からず、意味不明なものに感じられてしまうことが多いと思う。
  • 返信先のユーザと僕の関係が公開の場に出ているのはよくない場合もあるのではないか -- 僕はそういうことには頓着しない方だが、相手がそうかどうかは分からない。それに相手がTwitterをprotectedな設定で使っている場合は、やはり気を遣うべきなのではないか。もちろんTwitter上の僕のページを見に行けば分かることだけど、そういう人はTwitterのコミュニティにある程度入っている人だと考えられるから、ブログの方しか見ていない人とは違うと思う。

前述の通り、タイムラインを手元に持ってきて処理することにしたので、こういった処理はすぐに実現できる。最初は発言の中身を見て@で始まっていたら無視する、というようなことをしないといけないのかと思っていた。が、タイムラインとして取得できるオブジェクトの中に、 ``in_reply_to_id'' という要素があれば返信で、なければ返信ではないというのが分かったので、単純にこれを見て判断するようにした。実は、これについてはJavaScriptに手を入れるのでも実現することはできそうだ。 (が実際に試していないので分からない。)

TinyURLは展開して表示したい

Twitterに長めのURLを投稿すると、Twitterの側で自動的にTinyURLを使って短縮した上でタイムラインに投稿してくれるようになっている。SMSを使っていたり、その他諸々の状況においては確かにこれは有効なのだと思うけど、こうなってしまうとそのURLがいったい何を指しているのかはクリックしてみないと分からない。せっかく面白いものを見つけてつぶやいてみても、「なんだか分からないサイトで不安だからクリックしない方がいいな」という判断をされてしまう可能性は少なくないように思う。ということで、ブログで表示する時はTinyURLを元のURLに戻して表示するようにしたい。 (これもJavaScriptに手を入れることで実現できると思う。)

これは、CPANにあるWWW::Shortenというモジュールを使えば簡単に実現できた。

と書いて安心していたら、その直後くらいからTwitterがURL短縮にTinyURLだけではなくてbit.lyis.gdも使うようになっていることに気づいてしまった。しかし、WWW::Shorten::BitlyWWW::Shorten::isgdといったこれらのサービス用のPerlモジュールも存在するようなので、とりあえずは今後使われるサービスがさらに増えても大丈夫なような構造のスクリプトになるように意識した。 (実はこの記事の初稿を書き終えて公開しようと思った直後くらいにこのことに気づいたので、それからスクリプトを手直しして、この部分を書き足して、などということをしたので、ちょっと時間がたってしまった。)

URLをクリックできるようにしたい

これはもう特に説明の必要もないだろう。単にそうなっていた方が便利だというだけの話である。

最初は正規表現を書いて文字列を置換して...、などと考えていたのだが、ちょっと調べてみるとCPANのURI::Findというモジュールが便利に使えることが分かった。

新たな問題

と、ここまでで当初考えていた問題は全て解決できたのだが、結果として新たな問題が浮上した。というのは、長いURLが表示される時にも折り返されないので、見た目がよろしくないという問題だ。CSSをうまく設定することで回避できそうな気もするが、面倒だったのでURLをリンク化する処理のついでに、URLが一定以上の長さの時にはsubstr()を使って最初の部分だけを取り出し、その後に「......」を追加する、という処理もするようにした。

ブログへの設置

まずこのスクリプトを、たとえばget_twitter.cgiなどの適当な名前でドキュメント・ルート以下のどこかに保存する。Apache上のMovableType 4の場合は、このスクリプトを実行するSSIだけを書いたものを新しいウィジェット・テンプレートとして作り、サイドバーに追加してやればよいだろう。ただ、サーバによってはSSIが使えない場合もあるだろう。その場合はiframeを使ってこの出力を埋め込むという方法も考えられる。ただ、その場合はJavaScriptの場合ほどではないと思うが、Twitter部分の表示に時間がかかると、スクリーン・リーダーのユーザーにはうれしくない結果をもたらす可能性が出てくると思う。が、そもそもSSIが使えなければCGIも設置できないようになっているものなのかもしれない。 (僕は自分が管理するサーバでしかブログを運用したことがないので、このあたりはよく分からない。)

サンプル・コード

あまり人様にお見せできるようなコードではないのだが、参考にしていただける場合もあるかもしれないので、作ったものを以下に貼り付けることにする。コードの改善のアドバイスなどあれば、ぜひコメントを寄せていただければと思う。なお、このコードでは (そしてこの記事投稿時点でこのサイトで動作しているスクリプトでは) 、TinyURLとbit.lyだけに対応していて、is.gdは未対応。 (スクリプトの改造はすぐにできると思うが、その前にWWW::Shorten::isgdのFreeBSD用のPortを作りたいので、とりあえず現時点でのものを紹介する。)

#!/usr/bin/perl -w

use strict;
use utf8;

use Net::Twitter;
use Storable qw(lock_store lock_retrieve);
use Date::Parse;
use URI::Find;
use WWW::Shorten::TinyURL;
use WWW::Shorten::Bitly;

# 基本設定
#
# キャッシュファイルの場所
our $cachefile = "/usr/local/www/htdocs/cache/twitter";

# 自分の Twitterのユーザ名
our $twitter_userid = 'twitter_userid';

# bit.lyのユーザ情報
our $bitly_userid = 'bitly_userid';
our $bitly_apikey = 'bitly_apikey';

# 前回のアクセスからの経過時間がこれ以下の時にはキャッシュを利用
our $twitter_access_interval = 180;

# --------------------------------------------------------------------

my $lastaccess = 0;
my $cache;
my $status_arg = {id => $twitter_userid};

my $current_status;
if ( -f $cachefile ) {
  $cache = lock_retrieve($cachefile);
  $lastaccess = $cache->{lastaccess};  
  $status_arg = {id => $twitter_userid,
		 since_id => $cache->{list}[0]->{id}};
  $current_status = $cache->{list};
} else {
  $cache = {};
}

if ( time - $lastaccess > $twitter_access_interval ) {
  my $tw = new Net::Twitter({
			     username => '',
			     password => '',
			    skip_arg_validation => 1,
			    });
  my $tl = $tw->user_timeline($status_arg);

  if ( scalar(@$tl) > 0 ) {
    my $urlfinder = URI::Find->new(\&expand_url);;

    foreach my $f (@$tl) {
      $urlfinder->find(\$f->{text});
    }
    my @new_status = @$tl;
    push @new_status, @$current_status if defined($current_status->[0]);

    $cache->{list} = \@new_status;
    $cache->{lastaccess} = time();
    lock_store ($cache, $cachefile);
  }
}

foreach my $f (@{$cache->{list}}) {
  next if defined($f->{'in_reply_to_user_id'});

  my $timeval = time - str2time($f->{'created_at'});
  my $timestr;
  if ( $timeval < 60 ) {
    $timestr = $timeval . "秒前のつぶやき";
  } elsif ( $timeval < 3600 ) {
    $timestr = int($timeval / 60) . "分くらい前のつぶやき";
  } elsif ( $timeval < 86400 ) {
    $timestr = int($timeval / 3600) . "時間くらい前のつぶやき";
  } else {
    $timestr = int($timeval / 86400) . "日くらい前のつぶやき";
  }
  binmode(STDOUT, 'utf8');
  my $text = << "EOM";
Content-type: text/html

<div class="twitter widget">
<h2 class="widget-header"><a href="http://www.twitter.com/$twitter_userid/">Twitter:</a></h2>
<p class="widget-content">
$f->{text}  ($timestr)
</p>
</div>
EOM

  print $text;
  last;
}

sub expand_url {
  my ($u1, $u2) = @_;
  my $expand;
  if ( $u2 =~ m(^http://.*tinyurl\.com/.+)i ) {
    $expand = WWW::Shorten::TinyURL::makealongerlink($u2);
  } elsif ( $u2 =~ m(^http://bit\.ly/.+)i ) {
    $expand = WWW::Shorten::Bitly::makealongerlink($u2, $bitly_userid, $bitly_apikey);
  } else {
    $expand = $u2;
  }
  my $display = (length($expand) > 30)?
    (substr($expand, 0, 26) . "......"): $expand;

  return qq|<a href="$expand">$display</a>|;
}