2009年9月アーカイブ

RSSリーダとして、オープンソース版のfastladderを使っている。
fastladder は https のクライアント認証が必要な場所に置いてあって、標準のcrawlerではなくPlagger の Store::Fastladder を使って feed を入力している。
外出中は主にiPhoneを利用しているのだけれど、iPhoneで自前のクライアント証明書を入れる方法がわからなくて使えなかった。
今回、久しぶりに検索してみたら、iPhone 3Gでユーザー証明書を利用してみる - iPhone用連絡先交換サービス(QR Card)& 設定情報等 | iPhone Lab.と言う記事を発見した。
リンク先を見たら、既に「iPhone Configuration Utility 1.0 for Mac OS X」はリンク切れになっていたけれど、検索したらiPhone Configuration Utility 2.0 for Windowsと言うのを見つけたので、早速ダウンロードして使ってみた。
iPhone用のクライアント証明書と、オレオレCAの証明書をiPhone Configuration Utilityにimportして、構成ファイルを作成し、iPhoneにメールする。クライアント証明書をimportするときに、「export可能にする」にチェックしないと構成ファイルに秘密鍵を含めることができない。
iPhone側でメールに添付されたファイルをインストールすると、無事にiPhoneでクライアント証明書が使えるようになった。
しかし、fastladderはデスクトップPCでキーボードを使って操作することを前提に作られているので、iPhoneでは使い物にならない。
以前、fastladder_mobileと言うのを入れていたので、それで見てみたらなんとか見れた。
しかし、cssが適用されないっぽい。どうも、iPhone の Safari はサーバの証明書のホスト名が一致しないと css や javascript を実行してくれないようだ。
なんでホスト名が一致しないかと言うと、以下の理由による。
  • Apache の名前ベースのVirtualHostは、ホスト名ごとに別のサーバ証明書を扱えない(ので、サーバ証明書の名前はwww.wizard-limit.net)
  • fastladder は、コンテキストパスが / でないと実行できないため、メインのWebサーバとは別のホスト名を割り当てている
しょうがないので、https ではなく http 側も fastladder を有効にして、digest認証をかけてみた。
これで、iPhoneから見られるようになったのだが、fastladder_mobile にはいくつか気に入らないところが。
  • itemの並び順が、昇順ではなく降順
  • 設定で1ページの上限件数が指定できるが、一つのfeedに上限件数以上があると、その分は表示されてしまう。
ソースを少しいじって昇順に並ぶようにして、上限件数で切るようにしてみたが、問題が残った。
たとえば、あるfeedに未読が200件あったとして、最初の30件を表示させて、次のページに行くと200件全てが既読になってしまうのだ。
これは、fastladder の未読管理の仕組みに問題がある。
  • subscriptionsと言うテーブルにviewed_onと言う最後に読んだ日時と、has_unread と言うフラグがある
  • items と言うテーブルに stored_onと言う取得日時がある。
  • 既読にする処理は、subscriptionsのviewed_onを現在日時にして、has_unread を false にすると言う処理のため、全てが既読になってしまう
デスクトップのUIでは、When to mark a feed as read と言う設定がImmediately after loading.となっていればほぼ問題は起きないが、それ以外の設定だと問題が置きそうだ。
iPhoneや携帯では、1ページあたりの件数はある程度の数に限定したい。
そこで、今までRailsがわからないからと避けていたのだけれど、iPhone用になんとかしてみることにした。
iPhone対応にすると言うことで、iuiとかiWebKitとか試してみたんだけど、どちらもあまり気に入らなかった(iuiはアニメーションが遅かった、iWebKitはチェックボックスがimageを使ったトグルボタンになってしまう)のでiWebKitをベースに適当に切り貼りして作った。
後にして思えば、rep2 の iPhone 用UIが良くできているので、それを参考にするべきだったかも知れない。
最終的に採用したアルゴリズムは、以下の通り。
  • トップページは、未読があるfeedのリストをメニューとして表示する
  • feedを選択すると、item一覧ページを表示する
  • item一覧ページは、設定にある上限件数までの表示を目指すが、DBの未読管理の問題から同じstored_onを持つitemは全て表示する
  • item一覧ページから次のページに遷移するときに、表示していた最終stored_onでviewed_onを更新し、最終stored_onよりも新しいitemがないときだけhas_unreadをfalseにするようにする
  • 同じfeedに未読があるときは次のページも同じfeed。なければ次のfeed、次のfeedもなければHomeに帰る
複数ページあっても一気に既読にする機能とか、いろいろつけたい機能はあるけど、とりあえず読めるようになっちゃったからしばらくいじらないような気もする。
ソースは、fastladder_mobileに倣ってCodeReposに入れようかとも思ったけど、アカウント取るのが面倒だったのでとりあえずうちに入れておいた。
feeds.jpgitems.jpg

最近open fastladderがやけに遅い。
MySQLのチューニングなんかを見て、少しmy.cnfをいじってみたりしたけど効果なし。
itemsテーブルから古い記事を消すと少しだけ効果があるかな?と言う感じ。
そもそもMySQLは慣れていないので、古い記事を消すとかでも苦労しちゃう。
で、RrailsのActiveRecord?はPostgreSQLに対応しているんじゃないかと思って調べて見たら、いけそうな雰囲気。
まずは、fastladder/config/database.yml を修正。
production:
  adapter: postgresql
  encoding: utf8
  database: fastladder_production
  username: fastladder
  password: password
  host: localhost
変えたのは、adapterをmysqlからpostgresqlにして、socketをhostに変えただけ。
pgsqlユーザでDB fastladder_productionとユーザfastladderを作成して、rake db:migrateしてみたら、なんかpostgresがないみたいなエラーが出たので、
# gem install postgres
としてpostgresをインストールしたら、無事にrakeでテーブルが作られた。
あとは、PlaggerのStore::Fastladderなんだけど、こちらも中身を見たら直接SQLなんて書いてなくて、DBIx::Class::Schema::Loader とか言うのを使ってORマッピングしてるっぽい。
手元の環境を見たら、p5-DBD-Pgと言うのが入っていたので、Plaggerのyamlを修正してみる。
  - module: Store::Fastladder
    config:
      connect_info:
        - dbi:Pg:dbname=fastladder_production
        - fastladder
        - password
      member_id: 1
dbi:mysqlをdbi:Pgに変更して、後は接続パラメータの指定方法が違うのを適当に修正した。
plaggerを実行してみると、membersにid=1の人なんていないってエラーになるので、MySQLからmembersテーブルだけPostgreSQLに移行。MysqlからPostgresへ移行 - かぴぶろぐを参考にしたけど、記事の通りだとデータ中の改行が消えてしまうのでsedを使った。
  1. mysqlで実行(DBの管理者ユーザでやる)
    # mysql -u root fastladder_production -p
    Enter password:
    Reading table information for completion of table and column names
    You can turn off this feature to get a quicker startup with -A
    
    Welcome to the MySQL monitor.  Commands end with ; or \g.
    Your MySQL connection id is 1604
    Server version: 5.0.77 FreeBSD port: mysql-server-5.0.77_1
    
    Type 'help;' or '\h' for help. Type '\c' to clear the buffer.
    
    mysql> select * from members into outfile '/tmp/members.csv' fields terminated by ',' optionally enclosed by '\"';
    Query OK, 1 row affected (0.02 sec)
    
  2. ファイルを覗くと、データ中の改行が\+改行になっているので、\を削除する。
    # sed 's/\\$//' < /tmp/members.csv > /tmp/members2.csv
    
  3. PostgreSQL側で実行(こちらもDB管理者ユーザで)
    % psql -U pgsql fastladder_production -W
    Password for user pgsql:
    Welcome to psql 8.1.17, the PostgreSQL interactive terminal.
    
    Type:  \copyright for distribution terms
           \h for help with SQL commands
           \? for help with psql commands
           \g or terminate with semicolon to execute query
           \q to quit
    
    fastladder_production=# copy members from '/tmp/members2.csv' with csv null as '\\N' escape '\\';
    
  4. 再度plaggerを実行すると、今度はDateTime/Format/Pgがないと怒られたので、portsから devel/p5-DateTime-Format-Pg をインストール。
    今度こそうまくPostgreSQLにデータが入った。
    面倒なので過去のデータはそのまま捨てる(と言うかMySQLに入れたまま)にして、これから先のデータだけPostgreSQLを使うことにする。
    しかし、あっけないほど簡単だった。今回対象にした、fastladderとPlagger::Plugin::Store::Fastladderがそれぞれ特定のDBに依存していなかったおかげだ。
    RubyとかPerlは、普通にORマッピングを使ってるのに対して、Javaだとまだまだ閾が高いような気がするのは何故だろうか。

    次は、mongrel捨ててPassengerへの移行だな。
今まで mongrel を使って、Apache の mod_proxy でやっていたのですが、Redmine をインストールするときに知った Passenger を使ってみることにしました。
会社マシンに Passenger を入れたときにはいろいろモジュールが足りなくて何度もインストーラに怒られたのですが、自宅マシンだと一発で入りました。
あまりに簡単で記事にするほどでもないのですが、備忘録に。
# gem install passenger
Building native extensions.  This could take a while...
Successfully installed passenger-2.2.5
1 gem installed
Installing ri documentation for passenger-2.2.5...
Installing RDoc documentation for passenger-2.2.5...
# passenger-install-apache2-module
Welcome to the Phusion Passenger Apache 2 module installer, v2.2.5.

This installer will guide you through the entire installation process. It
shouldn't take more than 3 minutes in total.

Here's what you can expect from the installation process:

 1. The Apache 2 module will be installed for you.
 2. You'll learn how to configure Apache.
 3. You'll learn how to deploy a Ruby on Rails application.

Don't worry if anything goes wrong. This installer will advise you on how to
solve any problems.

Press Enter to continue, or Ctrl-C to abort.


--------------------------------------------

Checking for required software...

 * GNU C++ compiler... found at /usr/bin/g++
 * Ruby development headers... found
 * OpenSSL support for Ruby... found
 * RubyGems... found
 * Rake... found at /usr/local/bin/rake
 * Apache 2... found at /usr/local/sbin/httpd
 * Apache 2 development headers... found at /usr/local/sbin/apxs
 * Apache Portable Runtime (APR) development headers... found at /usr/local/bin/apr-1-config
 * Apache Portable Runtime Utility (APU) development headers... found at /usr/local/bin/apu-1-config

--------------------------------------------
(・・mod_passengerのコンパイル&インストール・・)
--------------------------------------------
The Apache 2 module was successfully installed.

Please edit your Apache configuration file, and add these lines:

   LoadModule passenger_module /usr/local/lib/ruby/gems/1.8/gems/passenger-2.2.5/ext/apache2/mod_passenger.so
   PassengerRoot /usr/local/lib/ruby/gems/1.8/gems/passenger-2.2.5
   PassengerRuby /usr/local/bin/ruby18

After you restart Apache, you are ready to deploy any number of Ruby on Rails
applications on Apache, without any further Ruby on Rails-specific
configuration!

Press ENTER to continue.


--------------------------------------------
Deploying a Ruby on Rails application: an example

Suppose you have a Ruby on Rails application in /somewhere. Add a virtual host
to your Apache configuration file, and set its DocumentRoot to
/somewhere/public, like this:

   
      ServerName www.yourhost.com
      DocumentRoot /somewhere/public    # <-- be sure to point to 'public'!
   

And that's it! You may also want to check the Users Guide for security and
optimization tips and other useful information:

  /usr/local/lib/ruby/gems/1.8/gems/passenger-2.2.5/doc/Users guide Apache.html

Enjoy Phusion Passenger, a product of Phusion (www.phusion.nl) :-)
http://www.modrails.com/

Phusion Passenger is a trademark of Hongli Lai & Ninh Bui.
後は、passenger-install-apache2-module に表示されたとおりに apacheの設定ファイルを変えるだけ。
もともと mongrel に飛ばす設定が書いてあったので、LoadModule からの3行の追加とProxyPass* の行を消すだけであっけなく動きました。
後は、mongrelを止めて終わりです。
Railsアプリをちょっといじったときは、$RAILS_ROOT/tmp/restart.txt と言うファイルを作成すればそのアプリだけ再起動してくれるそうです。
iPhoneで快適にfastladderが見れるようになったのだが、mixiの画像がうまく見れない。
これは、以前やった自前改造で、mixiの画像はobjectタグでmixiの画像表示ページへリンクするようにしていたのだけれど、iPhoneのSafariはページ内のobjectタグにスクロールバーが出ないので、画像の一部しか表示されない。
そこで、WWW::Mixi::ScraperとPlagger::Plugin::CustomFeed::MixiScraperに手を入れて、画像をローカル保存するようにして、fastladderから見れるようにしてみた。
しっかし、perl4のときにちょっとかじったくらいの知識だと、WWW::Mixi::Scraperのソースは暗号にしか見えない。YAPCの記事なんか見ててもうらやましくてしょうがないので、一度perlを勉強しなおした方が良いかも。(でも、Rubyもやりたいし・・・)

mixiの画像は、ページ表示時にサムネールの一時URLと、画像表示用ページのpermalinkを 返してくれる。画像表示用ページ(show_*_picture.pl)にアクセスすると、これまた画像の一時URLを返してくれる。
これは、前にmixiからの画像流出騒ぎ(マイミクにしか公開していない日記の写真が、他のメンバに見えてしまう)のときに取られた対応からこうなってるんだけど、こんな面倒なことしなくても画像を返すURLを*.plにして毎回権限チェックをすれば良いんじゃないかと思うんだけど、それだとサーバ負荷が問題になるんだろうか。
とにかく、一時URLを使われてしまうと、plaggerがcrawlした時間とfastladderで閲覧する時間に開きがあると画像が見えない。
そこで、日記やトピックの画像があったら画像表示用ページにアクセスして画像をローカルに保存しようと言うわけ。

そのために、まずは、WWW::Mixi::Scraper。こいつは、urlを渡すとscrapingしてハッシュ?を返してくれる。
Pluginを追加して、画像表示ページにアクセスしたら画像そのものを取ってくるようにする。
日記の場合は、http://mixi.jp/show_diary_picture.plと言うURLになるので、それ用のPluginを作る。
package WWW::Mixi::Scraper::Plugin::ShowDiaryPicture;

use strict;
use warnings;
use WWW::Mixi::Scraper::Plugin;

validator {qw(
  id        is_number
  owner_id  is_number
  number    is_number
)};

sub scrape {
  my ($self, $html) = @_;

  $scraper = scraper {
    process 'img',
      link => '@SRC';
    result qw ( link nil );
  };

  my $stash = $self->post_process($scraper->scrape(\$html))->[0];
  if ($stash->{link}) {
      $stash->{image} = $self->{mech}->get_content($stash->{link});
  }
  return $stash;
}

1;

__END__
Pluginの作り方は、Plugin.pmに書いてある。qw がなんだかわからなかった(調べろ→自分)んだけど、post_process() は配列を求めているのに、qw の中身が一つだと配列にならないらしくて怒られたので、ダミーを入れてみた。
同じように、コミュニティのトピックとトピックコメント用のPluginも作る。
package WWW::Mixi::Scraper::Plugin::ShowBbsPicture;

use strict;
use warnings;
use WWW::Mixi::Scraper::Plugin;

validator {qw(
  id        is_number
  comm_id  is_number
  number    is_number
)};

sub scrape {
  my ($self, $html) = @_;

  $scraper = scraper {
    process 'img',
      link => '@SRC';
    result qw ( link nil );
  };

  my $stash = $self->post_process($scraper->scrape(\$html))->[0];
  if ($stash->{link}) {
      $stash->{image} = $self->{mech}->get_content($stash->{link});
  }
  return $stash;
}

1;

__END__
package WWW::Mixi::Scraper::Plugin::ShowBbsCommentPicture;

use strict;
use warnings;
use WWW::Mixi::Scraper::Plugin;

validator {qw(
  id       is_number
  bbs_id   is_number
  comm_id  is_number
  number   is_number
)};

sub scrape {
  my ($self, $html) = @_;

  $scraper = scraper {
    process 'img',
      link => '@SRC';
    result qw ( link nil );
  };

  my $stash = $self->post_process($scraper->scrape(\$html))->[0];
  if ($stash->{link}) {
      $stash->{image} = $self->{mech}->get_content($stash->{link});
  }
  return $stash;
}

1;

__END__
validator のところ(getパラメータ)以外はまったく同じなので、もうちょっとなんとかしたいところだけど、perlのこの辺の仕組みがわからないのでなんともならなかった。
余談だけど、Scraper.pmのnewを見ると、上記Pluginを書くだけで
my $pic = $mixi->parse("/show_diary_picture.pl?owner_id=$oid&id=$id&number=$number");
my $pic = $mixi->show_diary_picture->parse( owner_id => $oid, id => $id, number => $number );
のいずれの書き方でも同じ動作をする理由がわかる。こう言うのがさらっと書けるようになると楽しいんだろうな。
これで、画像を取ってくる下準備はできたので、次はCustomFeed::MixiScraper。
あまりにやっつけで公開したくないんだけど、まあ参考程度に。
長いので、全文ではなくhttp://svn.bulknews.net/repos/plagger/trunk/ との差分で。差分を取ったときのリビジョンは2063。(たぶん、ベースにしたバージョンがもっと古いので、最新のものより機能足りないっぽい。最新取り直してやり直した方が良いかなあ。)
Index: MixiScraper.pm
===================================================================
--- MixiScraper.pm	(リビジョン 2063)
+++ MixiScraper.pm	(作業コピー)
@@ -74,7 +74,6 @@
       email => $self->conf->{email},
       password => $self->conf->{password},
       cookie_jar => $cookie_jar,
-      mode => $self->conf->{mode},
     );
 
     my $feed = Plagger::Feed->new;
@@ -86,15 +85,13 @@
     my($self, $context, $args) = @_;
     for my $type (@{$self->conf->{feed_type} || ['FriendDiary']}) {
         $context->error("$type not found") unless $MAP->{$type};
-        if ($type eq 'BBS' and $self->conf->{split_bbs_feed}) {
-            $self->aggregate_bbs_feed($context, $type, $args);
-        }
-        else {
-            $self->aggregate_feed($context, $type, $args);
-        }
+	if ($type eq 'BBS') {
+	    $self->aggregate_feed_bbs($context, $type, $args);
+	} else {
+	    $self->aggregate_feed($context, $type, $args);
+	}
     }
 }
-
 sub aggregate_feed {
     my($self, $context, $type, $args) = @_;
 
@@ -102,6 +99,8 @@
     $feed->type('mixi');
     $feed->title($MAP->{$type}->{title});
 
+    my $format = DateTime::Format::Strptime->new(pattern => '%Y-%m-%d %H:%M');
+
     my $meth = $MAP->{$type}->{get_list};
     my @msgs = $self->{mixi}->$meth->parse;
     my $items = $self->conf->{fetch_items} || 20;
@@ -110,148 +109,274 @@
     $feed->link($self->{mixi}->{mech}->uri);
 
     my $i = 0;
-    $self->{blocked} = 0;
+    my $blocked = 0;
     for my $msg (@msgs) {
         next if $type eq 'FriendDiary' and $msg->{link}->query_param('url'); # external blog
         last if $i++ >= $items;
 
-        $self->add_entry( $context, $type, $feed, $msg );
+        my $entry = Plagger::Entry->new;
+        $entry->title($msg->{subject});
+        $entry->link($msg->{link});
+        $entry->author($msg->{name});
+        $entry->date( Plagger::Date->parse($format, $msg->{time}) );
+	if ($entry->date) {
+	    $entry->date->set_time_zone('Asia/Tokyo');
+	}
+        if ($self->conf->{show_icon} && !$blocked && defined $MAP->{$type}->{icon}) {
+            my $owner_id = $msg->{link}->query_param($MAP->{$type}->{icon});
+            $context->log(info => "Fetch icon of id=$owner_id");
+
+            my $item = $self->cache->get_callback(
+                "outline-$owner_id",
+                sub {
+                    Time::HiRes::sleep( $self->conf->{fetch_body_interval} || 1.5 );
+                    my $item = $self->{mixi}->show_friend->parse(id => $owner_id)->{outline};
+                    $item;
+                },
+                '12 hours',
+            );
+            if ($item && $item->{image} !~ /no_photo/) {
+                # prefer smaller image
+                my $image = $item->{image};
+                   $image =~ s/\.jpg$/s.jpg/;
+                $entry->icon({
+                    title => $item->{name},
+                    url   => $image,
+                    link  => $item->{link},
+                });
+            }
+        }
+
+        my @comments;
+        if ($self->conf->{fetch_body} && !$blocked && $msg->{link} =~ /view_/ && defined $MAP->{$type}->{get_detail}) {
+            # view_enquete is not implemented and probably
+            # won't be implemented as it seems redirected to
+            # reply_enquete
+            next if $msg->{link} =~ /view_enquete/;
+            $context->log(info => "Fetch body from $msg->{link}");
+            my $item = $self->cache->get_callback(
+                "item-".$msg->{link},
+                sub {
+                    Time::HiRes::sleep( $self->conf->{fetch_body_interval} || 1.5 );
+                    my $item = $self->{mixi}->parse($msg->{link});
+                    $item;
+                },
+                '12 hours',
+            );
+            if ($item) {
+                my $body = $item->{description};
+                   $body =~ s!(\r\n?|\n)!<br />!g;
+                for my $image (@{ $item->{images} || [] }) {
+		    my $imagelink = $image->{link};
+		    $imagelink =~ s!.*(show_.*?_picture\.pl.*?)'.*!http://mixi.jp/$1!;
+		    if ( $self->conf->{fetch_image} ) {
+			my $pic = $self->{mixi}->parse($imagelink);
+			if ($pic->{image}) {
+			    my $filename = $pic->{link};
+			    $filename =~ s!.*/!!;
+			    open(OUT, ">" . $self->conf->{fetch_image_path} . "/images/mixi/diary/" . $filename);
+			    print OUT $pic->{image};
+			    close(OUT);
+			    my $imagepath = "/images/mixi/diary/$filename";
+			    $body .= qq(<a href="$imagelink"><img src="$imagepath" /></a>);
+			} else {
+			    $body .= qq(<a href="$imagelink"><object type="text/html" data="$imagelink" >$imagelink</object></a>);
+			}
+		    } else {
+			$body .= qq(<a href="$imagelink"><object type="text/html" data="$imagelink" >$imagelink</object></a>);
+		    }
+                }
+                $entry->body($body);
+
+                $entry->date( Plagger::Date->parse($format, $item->{time}) );
+		if ($entry->date) {
+		    $entry->date->set_time_zone('Asia/Tokyo');
+		}
+                if ($self->conf->{fetch_comment}) {
+                  for my $comment (@{ $item->{comments} || [] }) {
+                      my $c = Plagger::Entry->new;
+                         $c->title($entry->title . ': '. $comment->{subject});
+                         $c->body($comment->{description});
+                         $c->link($comment->{link});
+                         $c->author($comment->{name});
+                         $c->date( Plagger::Date->parse($format, $comment->{time}) );
+		      if ($c->date) {
+			  $c->date->set_time_zone('Asia/Tokyo');
+		      }
+                      push @comments, $c;
+                  }
+                }
+            } else {
+                $context->log(warn => "Fetch body failed. You might be blocked?");
+                $blocked++;
+            }
+        }
+
+        $feed->add_entry($entry);
+        for my $comment ( @comments ) {
+            $feed->add_entry($comment);
+        }
     }
 
     $context->update->add($feed);
 }
 
-sub aggregate_bbs_feed {
+sub aggregate_feed_bbs {
     my($self, $context, $type, $args) = @_;
 
+
+    my $format = DateTime::Format::Strptime->new(pattern => '%Y-%m-%d %H:%M');
+
     my $meth = $MAP->{$type}->{get_list};
     my @msgs = $self->{mixi}->$meth->parse;
     my $items = $self->conf->{fetch_items} || 20;
     $self->log(info => 'fetch ' . scalar(@msgs) . ' entries');
 
+
     my $i = 0;
-    $self->{blocked} = 0;
+    my $blocked = 0;
     for my $msg (@msgs) {
-        next if $type eq 'FriendDiary' and $msg->{link}->query_param('url'); # external blog
         last if $i++ >= $items;
 
-        my $feed = Plagger::Feed->new;
-        $feed->type('mixi');
-        (my $subject = $msg->{subject}) =~ s/\(\d+\)$//;
-        (my $link = $msg->{link}) =~ s/&comment_count=\d*//;
-        $feed->title($subject);
-        $feed->description($MAP->{$type}->{title}.': '.$msg->{name});
-        $feed->link($link);
+	my $feed = Plagger::Feed->new;
+	$feed->type('mixi');
+	my $subject = $msg->{subject};
+	$subject =~ s/\([0-9]*\)$//;
+	$feed->title($subject);
+	$feed->description('Mixi: ' . $msg->{name});
+	my $link = $msg->{link};
+	$link =~ s/&comment_count=[0-9]*//;
+	$feed->link($link);
 
-        $self->add_entry( $context, $type, $feed, $msg );
+        my $entry = Plagger::Entry->new;
+        $entry->title($subject);
+        $entry->link($link);
+        $entry->author($msg->{name});
+        $entry->date( Plagger::Date->parse($format, $msg->{time}) );
+	if ($entry->date) {
+	    $entry->date->set_time_zone('Asia/Tokyo');
+	}
+        if ($self->conf->{show_icon} && !$blocked && defined $MAP->{$type}->{icon}) {
+            my $owner_id = $msg->{link}->query_param($MAP->{$type}->{icon});
+            $context->log(info => "Fetch icon of id=$owner_id");
 
-        $context->update->add($feed);
-    }
-}
-
-my $format = DateTime::Format::Strptime->new(pattern => '%Y-%m-%d %H:%M');
-
-sub parse_date {
-    my ($self, $datetime) = @_;
-
-    # Calendar doesn't have %H:%M part (spotted by id:mad-capone)
-    return unless defined $datetime;
-    $datetime .= ' 00:00' unless $datetime =~ /\d+:\d+$/;
-
-    Plagger::Date->parse($format, $datetime);
-}
-
-sub add_entry {
-    my ($self, $context, $type, $feed, $msg) = @_;
-
-    if ($type eq 'Log') {
-        $msg->{subject} = $msg->{time} . ' ' . $msg->{name};
-    }
-
-    my $entry = Plagger::Entry->new;
-    $entry->title($msg->{subject});
-    $entry->link($msg->{link});
-    $entry->author($msg->{name});
-    $entry->date( $self->parse_date($msg->{time}) );
-    $entry->date->set_time_zone('Asia/Tokyo') if $entry->date;
-
-    if ($self->conf->{show_icon} && !$self->{blocked} && defined $MAP->{$type}->{icon}) {
-        my $owner_id = $msg->{link}->query_param($MAP->{$type}->{icon});
-        $context->log(info => "Fetch icon of id=$owner_id");
-
-        my $item = $self->cache->get_callback(
-            "outline-$owner_id",
-            sub {
-                Time::HiRes::sleep( $self->conf->{fetch_body_interval} || 1.5 );
-                my $item = $self->{mixi}->show_friend->parse(id => $owner_id)->{outline};
-                $item;
-            },
-            '12 hours',
-        );
-        if ($item && $item->{image} !~ /no_photo/) {
-            # prefer smaller image
-            my $image = $item->{image};
-               $image =~ s/\.jpg$/s.jpg/;
-            $entry->icon({
-                title => $item->{name},
-                url   => $image,
-                link  => $item->{link},
-            });
+            my $item = $self->cache->get_callback(
+                "outline-$owner_id",
+                sub {
+                    Time::HiRes::sleep( $self->conf->{fetch_body_interval} || 1.5 );
+                    my $item = $self->{mixi}->show_friend->parse(id => $owner_id)->{outline};
+                    $item;
+                },
+                '12 hours',
+            );
+            if ($item && $item->{image} !~ /no_photo/) {
+                # prefer smaller image
+                my $image = $item->{image};
+                   $image =~ s/\.jpg$/s.jpg/;
+                $entry->icon({
+                    title => $item->{name},
+                    url   => $image,
+                    link  => $item->{link},
+                });
+            }
         }
-    }
 
-    my @comments;
-    if ($self->conf->{fetch_body} && !$self->{blocked} && $msg->{link} =~ /view_/ && defined $MAP->{$type}->{get_detail}) {
-        # view_enquete is not implemented and probably
-        # won't be implemented as it seems redirected to
-        # reply_enquete
-        return if $msg->{link} =~ /view_enquete/;
-        $context->log(info => "Fetch body from $msg->{link}");
-        my $item = $self->cache->get_callback(
-            "item-".$msg->{link},
-            sub {
-                Time::HiRes::sleep( $self->conf->{fetch_body_interval} || 1.5 );
-                my $item = $self->{mixi}->parse($msg->{link});
-                $item;
-            },
-            '12 hours',
-        );
-        if ($item) {
-            my $body = $item->{description};
-               $body =~ s!(\r\n?|\n)!<br />!g;
-            for my $image (@{ $item->{images} || [] }) {
-                $body .= qq(<div><a href="$image->{link}"><img src="$image->{thumb_link}" style="border:0" /></a></div>);
-                my $enclosure = Plagger::Enclosure->new;
-                $enclosure->url($image->{thumb_link});
-                $enclosure->auto_set_type;
-                $enclosure->is_inline(1);
-                $entry->add_enclosure($enclosure);
-            }
-            $entry->body($body);
+        my @comments;
+        if ($self->conf->{fetch_body} && !$blocked && $msg->{link} =~ /view_/ && defined $MAP->{$type}->{get_detail}) {
+            # view_enquete is not implemented and probably
+            # won't be implemented as it seems redirected to
+            # reply_enquete
+            next if $msg->{link} =~ /view_enquete/;
+            $context->log(info => "Fetch body from $msg->{link}");
+            my $item = $self->cache->get_callback(
+                "item-".$msg->{link},
+                sub {
+                    Time::HiRes::sleep( $self->conf->{fetch_body_interval} || 1.5 );
+                    my $item = $self->{mixi}->parse($msg->{link});
+                    $item;
+                },
+                '12 hours',
+            );
+            if ($item) {
+                my $body = $item->{description};
+                   $body =~ s!(\r\n?|\n)!<br />!g;
+		if ( $self->conf->{fetch_image} ) {
+		    while ($body =~ /<a href=.*?&#39;(.*?)&#39;.*?<\/a>/) {
+			   my $imagelink = "http://mixi.jp/" . $1;
+			      $imagelink =~ s!&#38;!&!g;
+			   my $pic = $self->{mixi}->parse($imagelink);
+			   if ($pic->{image}) {
+			       my $filename = $pic->{link};
+			       $filename =~ s!.*/!!;
+			       open(OUT, ">" . $self->conf->{fetch_image_path} . "/images/mixi/bbs/" . $filename);
+			       print OUT $pic->{image};
+			       close(OUT);
+			       my $imagepath = "/images/mixi/bbs/$filename";
+			       $body =~ s!<a href=.*?&#39;.*?&#39;.*?</a>!<a href="$imagelink"><img src="$imagepath"/></a>!;
+			   } else {
+			       $body =~ s!<a href=.*?&#39;(.*?)&#39;.*?</a>!<object type="text/html" data="http://mixi.jp/$1">http://mixi.jp/$1</object>!;
+			   }
+		    }
+		} else {
+		    $body =~ s!<a href=.*?&#39;(.*?)&#39;.*?</a>!<object type="text/html" data="http://mixi.jp/$1">http://mixi.jp/$1</object>!g;
+		}
+		$body =~ s!&#38;!&!g;
+                $entry->body($body);
 
-            $entry->date( $self->parse_date($item->{time}) );
-            $entry->date->set_time_zone('Asia/Tokyo') if $entry->date;
-            if ($self->conf->{fetch_comment}) {
-              for my $comment (@{ $item->{comments} || [] }) {
-                  my $c = Plagger::Entry->new;
-                     $c->title($entry->title . ': '. $comment->{subject});
-                     $c->body($comment->{description});
-                     $c->link($comment->{link});
-                     $c->author($comment->{name});
-                     $c->date( $self->parse_date($comment->{time}) );
-                     $c->date->set_time_zone('Asia/Tokyo') if $c->date;
-                  push @comments, $c;
-              }
+                $entry->date( Plagger::Date->parse($format, $item->{time}) );
+		if ($entry->date) {
+		    $entry->date->set_time_zone('Asia/Tokyo');
+		}
+                if ($self->conf->{fetch_comment}) {
+                  for my $comment (@{ $item->{comments} || [] }) {
+                      my $c = Plagger::Entry->new;
+                         $c->title($subject . ': '. $comment->{subject});
+		      $body = $comment->{description};
+		      if ( $self->conf->{fetch_image} ) {
+			  while ($body =~ /<a href=.*?&#39;(.*?)&#39;.*?<\/a>/) {
+			      my $imagelink = "http://mixi.jp/" . $1;
+			      $imagelink =~ s!&#38;!&!g;
+			      my $pic = $self->{mixi}->parse($imagelink);
+			      if ($pic->{image}) {
+				  my $filename = $pic->{link};
+				  $filename =~ s!.*/!!;
+				  open(OUT, ">" . $self->conf->{fetch_image_path} . "/images/mixi/bbs/" . $filename);
+				  print OUT $pic->{image};
+				  close(OUT);
+				  my $imagepath = "/images/mixi/bbs/$filename";
+				  $body =~ s!<a href=.*?&#39;.*?&#39;.*?</a>!<a href="$imagelink"><img src="$imagepath"/></a>!;
+			      } else {
+				  $self->log(debug => "bbs_comment_picture $1 something wrong");
+				  $body =~ s!<a href=.*?&#39;(.*?)&#39;.*?</a>!<object type="text/html" data="http://mixi.jp/$1">http://mixi.jp/$1</object>!;
+			      }
+			  }
+		      } else {
+			  $body =~ s!<a href=.*?&#39;(.*?)&#39;.*?</a>!<object type="text/html" data="http://mixi.jp/$1">http://mixi.jp/$1</object>!g;
+		      }
+		      $body =~ s!&#38;!&!g;
+                         $c->body($body);
+		      $link = $comment->{link};
+		      $link =~ s/&comment_count=[0-9]*//;
+                         $c->link($link);
+                         $c->author($comment->{name});
+                         $c->date( Plagger::Date->parse($format, $comment->{time}) );
+		      if ($c->date) {
+			  $c->date->set_time_zone('Asia/Tokyo');
+		      }
+                      push @comments, $c;
+                  }
+                }
+            } else {
+                $context->log(warn => "Fetch body failed. You might be blocked?");
+                $blocked++;
             }
-        } else {
-            $context->log(warn => "Fetch body failed. You might be blocked?");
-            $self->{blocked}++;
         }
-    }
 
-    $feed->add_entry($entry);
-    for my $comment ( @comments ) {
-        $feed->add_entry($comment);
+        $feed->add_entry($entry);
+        for my $comment ( @comments ) {
+            $feed->add_entry($comment);
+        }
+	$context->update->add($feed);
     }
 }
 
@@ -320,10 +445,6 @@
 With this option set, this plugin fetches users buddy icon from
 mixi.jp site, which makes the output HTML very user-friendly.
 
-=item split_bbs_feed
-
-With this option set, BBS feed will be split up. Defaults to 0.
-
 =item feed_type
 
 With this option set, you can set the feed types.
これで、plaggerのyamlのMixiScraperのところに
    config:
      fetch_image: 1
      fetch_image_path: /var/www/fastladder/public
 
とか書くと、$fetch_image_path/images/mixi に画像が保存される。
本来は、CustomFeed::MixiScraperの段階では Plagger::Entry に Plagger::Enclosureを入れておいて、Store::Fastladder でファイル出力した方が正しい(gmailとかにも添付で送れるはずだし)と思うんだけど、あくまでやっつけなので。(言い訳ばっかりだな)
2009年9月
    1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30      

このアーカイブについて

このページには、2009年9月に書かれたブログ記事が新しい順に公開されています。

前のアーカイブは2009年8月です。

次のアーカイブは2009年11月です。

最近のコンテンツはインデックスページで見られます。過去に書かれたものはアーカイブのページで見られます。

Powered by Movable Type 6.1.1