コマンドラインからXMPPにメッセージを投げてみる

環境であれこれ

昨晩ちょっと試していたら,ppc mac, osx 10.4 + perl 5.10の環境だとAnyEventモジュールがインストールできませんでした.POE::Component::JabberもTestはすべてOKなんだけど,プロセスが終了時に255を返してると文句を言って来るのでforce insatallしないとインストールできません.オフィスのintel mac, osx 10.4 + perl 5.8.8 だと両方ともインストールできたので,perl5.10に起因する問題かもしれません.

昔作ったirc-notify

以前notify_irc.plというスクリプトを作りました.これは,IRCに常駐するサーバスクリプトと,コマンドラインからサーバ経由でIRCにメッセージを投げるクライアントスクリプトからなるシステムです.普段便利に使っているので,これのXMPP版を作ってみようと思いました.POE::Component::JabberはexampleフォルダにあるXMPPClientというスクリプトでGoogleTlakにアクセスできたので,これをベースにしています.起動すると

Reference is already weak at /usr/local/lib/perl5/site_perl/5.8.8/POE/Filter/XML.pm line 82.

という警告がでますが,今の所理由と対処方法がわからないのでほったらかしています.

クライアント

クライアントスクリプトはこんな感じになります.起動時の引数でメッセージの投げ先とメッセージ本体を指定しています.

#!/usr/local/bin/perl
use strict;
use warnings;
use Encode qw (_utf8_on);
use POE::Component::IKC::ClientLite;

my $r = POE::Component::IKC::ClientLite::create_ikc_client(
    port    => 9999,
    ip      => "localhost",
    timeout => 5,
) || die "create_ikc\n";

my $to   = $ARGV[0];
my $text = $ARGV[1];
_utf8_on($text);
$r->post( 'notify_jabber/update', { TO => $to, TEXT => $text } );

即興で作ったので美しくないですが,やりたいことはわかっていただけるでしょうか.

サーバ

サーバ側スクリプトは以前のnotify_irc.plと同様にconfig.yamlから設定を読み込んで動きます.config.yamlは以下のように構造になります.

---
notify_jabber_daemon_host: localhost
notify_jabber_daemon_port: 9999
notify_jabber_server_ip: foo.hoge.co.jp
notify_jabber_server_port: 5222
notify_jabber_server_hostname: foo.hoge.co.jp
notify_jabber_server_username: bot 
notify_jabber_server_password: PASSWD
notify_jabber_server_resource: jabberBot

daemonなんとか」というキーはIKCに関連するパラメータで,「serverなんとか」というキーがXMPPに関連するパラメータです."notify_jabber_server_ip"と"notify_jabber_server_hostname"に同じ値を設定していますが,前者はサーバのIPアドレスか,サーバの名称を記入し,後者はXMPPのメッセージ中に含まれるJIDのホスト名を記入します.一般的には同じ値が入る事になるでしょう.

で,サーバ本体は以下のようになります.error_eventは元のスクリプトそのままです.POE::Component::Jabber::new()をbot_start()の中で実行しているのは,new()がcurrent Sessionを要求するためです.つまり,POE::Session::create()を実行したときに最初に遷移する,_start状態の中で呼び出しています.

#!/usr/local/bin/perl

use POE qw(
  Filter::XML::Node
  Filter::XML::Utils
  Component::IKC::Server
  Component::IKC::Specifier
  Component::Jabber
  Component::Jabber::Error
  Component::Jabber::ProtocolFactory
  Component::Jabber::Status
  Session
);
use POE::Filter::XML::NS qw/ :JABBER :IQ /;
use warnings;
use strict;

use Term::ANSIColor qw(:constants);
sub msg (@) { print GREEN, BOLD, " * ", RESET, "@_\n" }
sub err (@) { print RED,   BOLD, " * ", RESET, "@_\n" }

msg 'loading configureation';
require YAML;
my $config = { %{ YAML::LoadFile('config.yaml') || {} }, };

msg 'creating daemon component';
POE::Component::IKC::Server->spawn(
    port => $config->{notify_jabber_daemon_port},
    name => 'NotifyJabberBot',
);

msg 'creating kernel session';
my $server = POE::Session->create(
    inline_states => {
        _start       => \&bot_start,
        status_event => \&status_event,
        error_event  => \&error_event,
        update       => \&update_event,
    }
);

msg 'starging the kernel';
POE::Kernel->run();
msg 'exiting';
exit 0;

sub bot_start {
    my ( $kernel, $heap ) = @_[ KERNEL, HEAP ];

    msg 'creating server component';
    $heap->{'component'} = POE::Component::Jabber->new(
        IP             => $config->{notify_jabber_server_ip},
        Port           => $config->{notify_jabber_server_port},
        Hostname       => $config->{notify_jabber_server_hostname},
        Username       => $config->{notify_jabber_server_username},
        Password       => $config->{notify_jabber_server_password},
        RESOURCE       => $config->{notify_jabber_server_resource},
        Alias          => 'bot',
        ConnectionType => +XMPP,
        States         => {
            StatusEvent => 'status_event',
            InputEvent  => 'input_event',
            ErrorEvent  => 'error_event',
        }
    );

    msg "starting jabber sesion";
    $kernel->alias_set('notify_jabber');
    $kernel->call( IKC => publish => notify_jabber => ['update'] );
    $kernel->post( 'bot', 'connect' );
}

sub status_event() {
    my ( $kernel, $sender, $heap, $state ) = @_[ KERNEL, SENDER, HEAP, ARG0 ];

    if ( $state == +PCJ_INIT_FINISHED ) {
        $heap->{'jid'} = $heap->{'component'}->jid();
        $heap->{'sid'} = $sender->ID();
        $kernel->post( 'bot', 'output_handler',
            POE::Filter::XML::Node->new('presence') );
        $kernel->post( 'bot', 'purge_queue' );
    }
}

sub update_event() {
    my ( $kernel, $heap, $msg ) = @_[ KERNEL, HEAP, ARG0 ];

    my $node = POE::Filter::XML::Node->new('message');
    $node->attr( 'to',   get_bare_jid( $$msg{TO} ) );
    $node->attr( 'type', 'chat' );
    $node->insert_tag('body')->data( $$msg{TEXT} );
    $kernel->post( $heap->{'sid'}, 'output_handler', $node );
}

sub error_event() {
    my ( $kernel, $sender, $heap, $error ) = @_[ KERNEL, SENDER, HEAP, ARG0 ];

    if ( $error == +PCJ_SOCKETFAIL ) {
        my ( $call, $code, $err ) = @_[ ARG1 .. ARG3 ];
          err "Socket error: $call, $code, $err";
          err "Reconnecting!";
        $kernel->post( $sender, 'reconnect' );
    }
    elsif ( $error == +PCJ_SOCKETDISCONNECT ) {
          err 'We got disconneted';
          err 'Reconnecting!';
        $kernel->post( $sender, 'reconnect' );
    }
    elsif ( $error == +PCJ_CONNECTFAIL ) {
          err 'Connect failed';
          err 'Retrying connection!';
        $kernel->post( $sender, 'reconnect' );
    }
    elsif ( $error == +PCJ_SSLFAIL ) {
          err 'TLS/SSL negotiation failed';
    }
    elsif ( $error == +PCJ_AUTHFAIL ) {
          err 'Failed to authenticate';
    }
    elsif ( $error == +PCJ_BINDFAIL ) {
          err 'Failed to bind a resource';
    }
    elsif ( $error == +PCJ_SESSIONFAIL ) {
          err 'Failed to establish a session';
    }
}

おわりに

今回のスクリプトは相手に直接メッセージを送ることしかできません.相手がloginしてるかどうかの確認もしていませんし,グループチャットにメッセージを送ることもできません.そこまでやろうとするとPOE::Component::Jabberでは役不足でNet::XMPP2を使う方が色々ツールがそろっています.でもAnyEventよくわかってないので,PoCoみたいに機能単位でモジュール化がすでに行われているのかどうかがわかりません.機能単位のモジュールがあれば,ここで作ったようなスクリプトもすぐにできるでしょうね.飽きなければ,どっちかのモジュールを使ってもうすこし高機能なスクリプトを作ってみたいと思ってます.