October 20, 2003

Shibuya.pm でデモした POE による pingbot のソース

[ Perl , ウェブログに関すること ]

Shibuya.pm のプレゼンでデモに使った POE による Ping サーバ + IRC、そのソースコードを載せておきます。pingbot とか名前を付けてみました。

pingbot.png

POE で XML-RPC サーバと他のコンポーネントを組み合わせる話については、Blog Developer's CookbookWeblogs.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 Ito 
 use 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::SimplePOE::Component::Client::HTTP に変える、また HTML::RSSAutodiscovery の中でも LWP が使われているので、そこは自前で書くとかすれば良さそうです。

あるいは、そこの部分だけ別のスクリプトにしておき POE::Wheel::Run で実行してしまうという裏業もアリなんだとか。(宮川さん談)

2003.10.21 追記:

POE::Kernel->call は非同期ではなく、同期用のメソッドです。勘違いしてました。POE::Kernel->post は非同期且つ FIFO です。詳しくはコメント参照。

で、軽く修正したソースが以下です。これでいいのかな。

pingbot.pl

Posted by naoya at October 20, 2003 01:53 AM | トラックバック (2)  b_entry.gif
トラックバック [2件]
TrackBack URL: http://mt.bloghackers.net/mt/suck-tbspams.cgi/547
pingbot
Excerpt: NDO::Weblog: Shibuya.pm でデモした POE による pingbot のソース これは面白いですな。うちのIRCサーバにも入れてみたい。 Shibuya.pm 、興味はあったけど前回は気づいたら酸化締め切ってた。次があったら行ってみたいな。...
Weblog: wolog
Tracked: October 21, 2003 01:23 PM
BlogSurfのRSS取得プログラムをPOEで書き直す。
Excerpt: BlogSurfのRSS取得プログラムをPOEを使って書き直してみた。 POEとは、 multitasking and networking framework...
Weblog: blog.nomadscafe.jp
Tracked: September 21, 2004 11:55 PM
コメント [2件]

POE::Kernel の call() は基本的に synchonize して実行してしまいますので、返り値がすぐ欲しい場合とか以外には使わない方がいいですよ。

$kernel->post() でも基本的にはFIFOですので問題ないはず。もし本当に順番を保証したいなら、A イベントを post して、A イベントのレスポンスをうけとるハンドラで B を post する、みたいな流れにするといいはずです。

[1] Posted by: miyagawa at October 20, 2003 10:33 AM [返信]

>>1 miyagawa さん

call は同期呼び出しでしたね。勘違いしてました。ちと後で直しまする。ツッコミどうもです。:)

[2] Posted by: naoya at October 20, 2003 11:14 PM [返信]