2007-07-01
■ [tips] Publish::FTPでは越えられないSOCKSの壁をFFFTPの力を借りて越える

いくつか加筆訂正。
あと,実際に運用してみたところ,少なくとも私の環境では,ミラーリングアップロード時にFFFTPが固まってしまう問題が起きやすいことが分かった。どうしたものか。
きっかけ
現在の私にはちょっと理由があって,Plaggerが使える環境がFirewallの中にしかない状況にある。そしてFirewallの外に出るためには,HTTPならHTTPプロキシが用意されているのだが,それ以外のプロトコルについてはSOCKSサーバしか用意されていない。
このSOCKSサーバが曲者で,通常のNet::FTPではどんなにオプションを指定しても越えることができないらしく,越えるには専用のSOCKSライブラリをリンクしてperlをコンパイルしなければならないらしい。
この事実は「CustomFeed::GoogleCalendar→Publish::Feed→Publish::FTPで最新イベントをフィード配信だ!」みたいなことをもくろんでいた私の前に立ちはだかった。Linux環境ならまだしも,Windows環境でActivePerlを使っている自分にはきつい話だ。自宅鯖なんて管理が面倒なものを持つ気にもならない。
そこで考えたのが,FFFTPを利用したFTPアップロード。実はFFFTPはコマンドラインからも利用することが可能で,しかもSOCKSに対応している。perlの再コンパイルよりはるかに手軽だ。
FFFTPの設定
まず,FFFTPのメニュー「オプション」→メニュー「環境設定」→タブ「FireWall」からSOCKSの設定をする。この辺の設定はネットワーク管理者に聞こう。
次に接続ホストの設定。基本的にいつも通りだが,以下の3点に気をつける。
- 「基本」タブの「ホストの設定名」をメモっておく(以下「myhost」とする)
- 「基本」タブの「ローカルの初期フォルダ」「ホストの初期フォルダ」は,それぞれアップロードするファイルが置いてあるローカルの/置かれるホストのフォルダを指定しておく。
- 「拡張」タブの「FireWallを使う」チェックボックスをオンにしておく。
そしてFFFTPが存在するディレクトリにPATHを通す。これはWindowsの環境変数の設定から行う。
これで準備完了。コマンドラインから以下のように入力すると,FFFTPはmyhostの設定を使用して(-s "myhost")ミラーリングアップロードを行う(-m)。これはローカルとホストのフォルダの内容を同期させるものだ。
ffftp -s "myhost" -m
確認メッセージが出るので,内容をよく確認した後にアップロードを行おう。アップロードが終わったらFFFTPも終了しよう。
ミラーリングの対象となるフォルダは,先ほど接続ホストの設定で指定したローカルとホストのフォルダになる。したがって,このローカルフォルダをPublish::FeedやPublish::iCalなどの出力先としておけば,Plagger実行後に上記のFFFTPコマンドですかさずファイルをアップロードできるわけだ。
バッチ化
とは言っても,Plagger実行毎にコマンドを打つのも面倒なので,バッチ処理ができるようにしておこう。バッチ処理のためには以下の内容のファイルを用意する。ここではPlaggerのコンフィグファイルを仮にconfig.yamlとしており,このファイルはバッチファイルと同じディレクトリにあるものとする。
CALL plagger -c config.yaml CALL ffftp -s "myhost" -m -f -q
FFFTPの起動にさらにオプションが追加されているが,こうすると確認メッセージが表示されず強制的にミラーリングアップロードが行われ(-f),アップロード終了時にFFFTP自身も終了する(-q)。
あとはこのバッチファイルをwronに登録して定期的に実行すれば,いつでもできたて新鮮なフィードが得られるってわけだ。
この技を使って,近日中に本家ブログにカレンダーのフィードを設置する予定。
■ [CustomFeed] CustomFeed::GoogleCalendar

Googleカレンダーに限って言えばAtomを返すAPIを叩く方がより柔軟にデータの取得が出来ると思う。
つーか俺もプラグイン作る前に確認しようぜ。
iCal形式のファイルを取得し,イベントごとにエントリ化したフィードを生成する。
名前が”GoogleCalendar”となっているが,iCal形式であればおそらくGoogle Calendar以外でも取得・生成が可能だと思う。ただし確認はしていない。
Plagger/Plugin/CustomFeed/GoogleCalendar.pm
package Plagger::Plugin::CustomFeed::GoogleCalendar; use utf8; use strict; use base qw( Plagger::Plugin ); use Data::ICal; use DateTime::Format::ICal; use URI; use Plagger::Date; use Plagger::Util; sub register { my ($self, $context) = @_; $context->register_hook( $self, 'subscription.load' => \&load, ); } sub load { my ($self, $context) = @_; my $calendars = $self->conf->{calendar} or return; $calendars = [$calendars] unless ref $calendars; for my $calendar (@$calendars) { my $now = $calendar->{now} ? Plagger::Date->parse_dwim(delete $calendar->{now}) : Plagger::Date->now; my $feed = Plagger::Feed->new; $feed->aggregator( sub { $self->aggregate($context, $calendar, $now); }); $context->subscription->add($feed); } } sub aggregate { my ($self, $context, $config, $now) = @_; my $content = Plagger::Util::load_uri(URI->new($config->{url})) or return; my $calendar = Data::ICal->new(data => $content); unless ($calendar) { $context->error($calendar->error_message); return; } my $feed = Plagger::Feed->new; $feed->url($config->{url}); $feed->title($config->{title} || $self->unescape($calendar->property('x-wr-calname')->[0]->value)); $feed->description($config->{desc} || $self->unescape($calendar->property('x-wr-caldesc')->[0]->value)); my $events = $calendar->entries; my $timezone = shift @$events; for my $event (@$events) { my $data; for my $key (qw/ dtstart dtend uid summary description location /) { $data->{$key} = $event->property($key); $data->{$key} = $data->{$key}->[0]->value if $data->{$key}; # @ADHOC } my $allday = ($data->{dtstart} =~ /^\d{8}$/) && ($data->{dtend} =~ /^\d{8}$/); for my $key (qw/ dtstart dtend /) { $data->{$key} = DateTime::Format::ICal->parse_datetime($data->{$key}); } my $entry = Plagger::Entry->new; $entry->id($self->unescape($data->{uid})); $entry->title($self->unescape($data->{summary})); $entry->body($self->unescape($data->{description})); $entry->author($self->unescape($data->{location})); if (Plagger::Date->compare($now, $data->{dtstart}) < 0) { $entry->date($data->{dtstart}); $entry->add_tag('prep'); } else { $entry->date($allday ? $data->{dtend}->clone->subtract(days => 1) : $data->{dtend}); if (Plagger::Date->compare($now, $data->{dtend}) < 0) { $entry->add_tag('open'); } else { $entry->add_tag('close'); } } $feed->add_entry($entry); } $context->update->add($feed); } sub unescape { my ($self, $content) = @_; # to single line $content = join '', split /\r\n[ ]/, $content; my @unescaped = (); my $state = 0; for my $char (split //, $content) { if ($state == 1) { push @unescaped, ($char eq 'n' ? "\n" : $char); $state = 0; } elsif ($char eq "\\") { $state = 1; } else { push @unescaped, $char; } } join '', @unescaped; } 1;
エントリのauthorには,iCalで言うVEVENTのLOCATIONがセットされる。
開催前・開催中・開催終了のイベントのエントリには,それぞれprep,open,closeというタグが付与される。
config.calendar.yaml
この例では,取得した全てのイベントの中から,今週のイベントのみを残し,日付順に整列して,RSS形式で出力する。
plugins: - module: CustomFeed::GoogleCalendar config: calendar: - url: http://www.google.com/calendar/ical/hb3ceqibm3d7kbp99e61klnvpo%40group.calendar.google.com/public/basic.ics - module: Filter::Rule rule: - module: Expression expression: | use Plagger::Date; Plagger::Date->compare(Plagger::Date->today, $args->{entry}->date) <= 0 && Plagger::Date->compare($args->{entry}->date, Plagger::Date->today->add(days => 7)) < 0; - module: Filter::SortEntries config: expression: | use Plagger::Date; Plagger::Date->compare($a->date, $b->date); - module: Publish::Feed config: format: RSS dir: ./calendar filename: calendar.xml taguri_base: sweetpotato
Subscription::Configと同様,複数のカレンダーをconfigファイルに登録できる。その場合はひとつのカレンダーがひとつのフィードになる。
各カレンダーにはまた,上で述べた開催前・開催中・開催終了の「基準」となる日時を指定することもできる。指定しない場合は現在の日時が基準になる。
Filter::SortEntriesについては以下の記事を参照。
何ができるの?
例えば,カレンダーから今週のイベントのみをふるい分けて,フィードに出力して,そのフィードをJavaScriptで読み込んでブログの柱に表示する,とか。イベントの管理がカレンダーで一元管理できるようになるわけだ。
他には,カレンダーから今日の予定のみをふるい分けて,自分のメールアドレスに毎朝送るとか。リマインダー的な使い方。Google Calendarデフォルトのリマインダー機能よりもリッチな処理ができるはず。
でもPlaggerだからきっともっといろいろできるはずだよ!
補足
プラグイン名をCustomFeed::iCalにしなかった理由は,半分はGoogle Calendar以外のiCalでもちゃんとテストをすることが面倒だったことから。
もう半分は,iCalサポートは次のバージョンのPlaggerで既に予定されており,おそらくそうなるであろう名前と衝突するのを避けたかったから。これについてはmiyagawa氏の第9回XML開発者の日のスライド186枚目に:
Calendar Support
iCal parser & emitter
hCalendar microformats
.ics attached in emails
Sync::SyncML
no title
と書かれている。
■ [Filter] Filter::SortEntries

フィード内のエントリを並べ替えるFilter。
Plagger/Plugin/Filter/SortEntries.pm
package Plagger::Plugin::Filter::SortEntries; use strict; use base qw( Plagger::Plugin ); sub register { my ($self, $context) = @_; $context->register_hook( $self, 'update.feed.fixup' => \&update, ); } sub update { my ($self, $context, $args) = @_; return unless defined $self->conf->{expression}; my @entries = sort { eval $self->conf->{expression} } $args->{feed}->entries; $args->{feed}->{entries} = \@entries; } 1;
行儀が悪いにも程がある。
config.yaml
この例では日付が古い順に整列する。
# ... - module: Filter::SortEntries config: expression: | use Plagger::Date; Plagger::Date->compare($a->date, $b->date);
http://nyarla.net/blog/plagger24
nyarla-netさんは比較にcmpを使ってらっしゃいますが,これは日付(date)での整列も可能ですか?
僕の作ったプラグインの場合、指定されたプロパティを単純に比較しているだけなので、そのあたり込み入ったことが出来ないと思います。
まあSweetPotatoさんのか僕のかどちらかをベースにしてくっつけてしまえば良いと思いますが。