Shibuya.pm のプレゼンでデモに使った POE による Ping サーバ + IRC、そのソースコードを載せておきます。pingbot とか名前を付けてみました。
POE で XML-RPC サーバと他のコンポーネントを組み合わせる話については、Blog Developer's Cookbook の Weblogs.com Ping Gateway to はてなアンテナ (POE版) で宮川さんが丁寧に解説されてるので、ここでは省略。(楽ちんw)
POE::Component::Server::XMLRPC を使って Weblogs.Com Ping の Ping を (XML-RPC で)受信します。受信した XML-RPC リクエストにはパラメータとして、ウェブログの名前と URL が渡ってくるので、それを POE::Component::IRC によって実装される IRC bot に渡し、IRC チャンネルに発言を行います。
このとき、ウェブログの名前だけではなく、更新のあった記事のタイトルや permalink が分かると便利なので、Ping を受信した後に、Ping 送信元に対して HTTP でアクセスし、RSS-autodiscovery により RSS の URL を探し出して、その RSS から最新記事のタイトルを引っ張って来る、という処理もやってます。
XML-RPC サーバと IRC bot という役割の異なる二つのコンポーネントが POE カーネルを通してメッセージをやりとりして協調動作しています。メッセージのやり取りは post や call といったメソッド一行で、とても簡単です。
POE は最初はちょっと分かりにくいですが、一度分かると色々なアイデアが沸いてきてなかなか楽しいです。(see also 'NDO::Weblog: POE - Perl Object Environment に触れる')
#!/usr/local/bin/perl # $Id: pingbot,v 1.4 2003/09/27 18:15:55 naoya Exp $ # pingbot - Weblogs.Com Ping を受け取って IRC にメッセージを投げる # # XML-RPC URL ... http://localhost:10080/?session=pingbot # # Naoya Itouse strict; use warnings; # sub POE::Kernel::ASSERT_DEFAULT() { 1 }; # sub POE::Kernel::TRACE_DEFAULT () { 1 }; # sub POE::Kernel::TRACE_EVENTS() { 1 }; use POE; use POE::Component::IRC; use POE::Component::Server::XMLRPC; use POE::Component::TSTP; use POE::Sugar::Args; use HTML::RSSAutodiscovery; use LWP::Simple; use XML::RSS; use Lingua::JA::Regular; use String::Multibyte; use Encode; our $VERSION = "1.00"; our $CHANNEL = '#test'; our $NICK = 'pingbot'; our $USERNAME = 'pingbot'; our $IRCNAME = 'POE::Component::IRC pingbot'; our $SERVER = 'naoya.dyndns.org'; our $PORT = 6667; my $port = shift || 10080; # Ctrl-Z をトラップする POE::Component::TSTP->create; POE::Component::IRC->new("irc"); POE::Component::Server::XMLRPC->new( alias => "xmlrpc", port => $port ); POE::Session->create ( inline_states => { _start => \&setup_service, _stop => \&shutdown_service, 'weblogUpdates.ping' => \&ping_handler, irc_001 => \&on_connect, ping_to_irc => \&ping_to_irc, entry_to_irc => \&entry_to_irc, } ); POE::Kernel->run; exit 0; sub setup_service { my $poe = sweet_args; # XMLRPCサーバのセットアップ $poe->kernel->alias_set("pingbot"); $poe->kernel->post( xmlrpc => publish => pingbot => "weblogUpdates.ping" ); # IRC botのセットアップ $poe->kernel->post( irc => register => qw(001) ); $poe->kernel->post( irc => connect => { Nick => $NICK, Username => $USERNAME, Ircname => $IRCNAME, Server => $SERVER, Port => $PORT, }); } sub shutdown_service { my $poe = sweet_args; $poe->kernel->post( xmlrpc => rescind => weblogUpdates => "weblogUpdates.ping" ); } sub on_connect { my $poe = sweet_args; $poe->kernel->post( irc => join => $CHANNEL ); } sub ping_handler { my $poe = sweet_args; my $transaction = $poe->args->[0]; my ($weblog_name, $weblog_url) = @{$transaction->params}; # IRC に投げるメッセージの順番を保証するために post ではなく call (FIFO)$poe->kernel->call( "pingbot" => "ping_to_irc" => $weblog_name, $weblog_url ); $poe->kernel->call( "pingbot" => "entry_to_irc" => $weblog_name, $weblog_url); $transaction->return( { flerror => XMLRPC::Data->type('boolean', 0) , message => "Thanks for the ping" } ); } sub ping_to_irc { my $poe = sweet_args; my $weblog_name = $poe->args->[0]; my $weblog_url = $poe->args->[1]; $poe->kernel->call( irc => privmsg => $CHANNEL => sprintf("** Ping Received from %s!! (%s) **", $weblog_name, $weblog_url) ); } sub entry_to_irc { my $poe = sweet_args; my $weblog_name = $poe->args->[0]; my $weblog_url = $poe->args->[1]; my ($rss_url, $title, $permalink, $desc); eval { my $html = HTML::RSSAutodiscovery->new; if (my $result = $html->parse( $weblog_url ) ) { $rss_url = $result->[0]->{href}; } die "Could not determine RSS url. (source: $weblog_url)" if (not defined $rss_url); my $parser = new XML::RSS; my $rss = Lingua::JA::Regular->new( LWP::Simple::get( $rss_url ) )->regular; # my $rss = encode('UTF-8', $rss ); $parser->parse( $rss ); $title = $parser->{items}->[0]->{title} || ""; $permalink = $parser->{items}->[0]->{link} || ""; $desc = $parser->{items}->[0]->{description} || ""; }; if ($@) { print STDERR $@; } if ( $title and $permalink ) { $poe->kernel->call( irc => privmsg => $CHANNEL => encode('JIS', $title) ); $poe->kernel->call( irc => privmsg => $CHANNEL => $permalink ); } if ( $desc ) { my $mbcs = String::Multibyte->new('UTF8'); my $subdesc = $mbcs->substr($desc, 0, 30); $subdesc .= '...' if ( $mbcs->length($subdesc) < $mbcs->length($desc) ); $poe->kernel->call( irc => privmsg => $CHANNEL => encode('JIS', $subdesc) ); } }
本来は entry_to_irc の中で HTML 受信処理があり、そこでブロッキングが発生するので完全な非同期処理にする場合には、もうちょっと手を入れる必要があるようです。LWP::Simple を POE::Component::Client::HTTP に変える、また HTML::RSSAutodiscovery の中でも LWP が使われているので、そこは自前で書くとかすれば良さそうです。
あるいは、そこの部分だけ別のスクリプトにしておき POE::Wheel::Run で実行してしまうという裏業もアリなんだとか。(宮川さん談)
2003.10.21 追記:
POE::Kernel->call は非同期ではなく、同期用のメソッドです。勘違いしてました。POE::Kernel->post は非同期且つ FIFO です。詳しくはコメント参照。
で、軽く修正したソースが以下です。これでいいのかな。