GoogleTalkプログラミング

Google Talkが始まってそろそろ一ヶ月.使っているプロトコルFAQ: Open Communications  |  Google Talk for Developers  |  Google Developersにあるように,Jabberというオープンなプロトコルだということで,perlからアクセスしてみようと思って苦闘してました.

そう,苦闘です.Jabberを処理するモジュールはCPANを検索するといくつも見つかるんですが,名前がばっちりのNet::Jabberは,インストールしようと思ってもmake testでひっかかってしまってうまくインストールできません.NAT経由のcolinuxを使ってるからなのか,理由がさっぱりわからなくてお手上げ.そうかと思うと,Google Talkで必須のTLSをサポートしていないモジュールもあったりで,モジュールを選ぶところから苦労しました.で,最終的にPOE::Component::Jabberを使うことにしたんですが,POEをまじめに使うのは初めてだし,使えそうなサンプルもなくてわからないことだらけだったのでした.

そんなこんなでやっと動いたスクリプトはこんな感じです.まだまだいろいろ試している段階なので,サーバとの間でやりとりしているxmlデータを出力する機能しか作っていません.

#!/bin/perl

use POE::Preprocessor;
use POE qw/ Component::Jabber::Client::XMPP Component::Jabber::Error /;
use POE::Filter::XML::NS qw/ :JABBER :IQ /;
use POE::Filter::XML::Node;
const XNode POE::Filter::XML::Node
use encoding 'utf-8', STDOUT => 'shift-jis'; 
use warnings;
use strict;

POE::Session->create(
	inline_states => {
		_start => sub{
			my ($kernel, $heap) = @_[KERNEL, HEAP];
			$kernel->alias_set('SESSION');
			$heap->{'XMPPALIAS'} = 'XMPP';
			POE::Component::Jabber::Client::XMPP->new(
				IP => 'talk.google.com',
				PORT => '5222',
				HOSTNAME => 'gmail.com',
				USERNAME => 'whoami',
				PASSWORD => 'password',
				ALIAS => 'XMPP',
				STATE_PARENT => 'SESSION',
				STATES => {
					InitFinish => 'init_finished',
					InputEvent => 'input_event',
					ErrorEvent => 'error_event',
				}
			)
		},
		init_finished => \&init_finished,
		input_event => \&input_event,
		output_event => \&output_event,
		error_event => \&error_event,
		send_presence => \&send_presence,
	}
);

sub init_finished()
{
	my ($kernel, $heap, $jid) = @_[KERNEL, HEAP, ARG0];
	print "INIT FINISHED!\n";
	$heap->{'JID'} = $jid;
	$kernel->yield('send_presence', 'Available', 'perlbot');
}

sub send_presence()
{
	my ($kernel, $show, $status) = @_[KERNEL, ARG0, ARG1];
	my ($node);
	$node = XNode->new('presence');
	$node->insert_tag('show')->data($show) if $show;
	$node->insert_tag('status')->data($status) if $status;
	$kernel->yield('output_event', $node);
}

sub input_event()
{
	my ($kernel, $heap, $node) = @_[KERNEL, HEAP, ARG0];
	print "\n===PACKET RECEIVED===\n";
	print $node->to_str() . "\n";
	print "=====================\n\n";
}

sub output_event()
{
	my ($kernel, $heap, $node) = @_[KERNEL, HEAP, ARG0];
	print "\n===PACKET SENT===\n";
	print $node->to_str() . "\n";
	print "=================\n\n";
	$kernel->post($heap->{'XMPPALIAS'}, 'output_handler', $node);
}

sub error_event()
{
	my ($kernel, $sender, $heap, $error) = @_[KERNEL, SENDER, HEAP, ARG0];
	if($error == +PCJ_SOCKFAIL) {
		my ($call, $code, $err) = @_[ARG1..ARG3];
		print "Socket error: $call, $code, $err\n";
	} elsif($error == +PCJ_SOCKDISC) {
		print "We got disconneted\n";
		print "Reconnecting!\n";
		$kernel->post($sender, 'reconnect_to_server');
	} elsif ($error == +PCJ_AUTHFAIL) {
		print "Failed to authenticate\n";
	} elsif ($error == +PCJ_BINDFAIL) {
		print "Failed to bind a resource\n";
	} elsif ($error == +PCJ_SESSFAIL) {
		print "Failed to establish a session\n";
	}
}
POE::Kernel->run();

以下,自分のメモの意味を含めての解説です.まず先頭の部分ですが,

#!/bin/perl
use POE::Preprocessor;
use POE qw/ Component::Jabber::Client::XMPP Component::Jabber::Error /;
use POE::Filter::XML::NS qw/ :JABBER :IQ /;
use POE::Filter::XML::Node;
const XNode POE::Filter::XML::Node
use encoding 'utf-8', STDOUT => 'shift-jis'; 
use warnings;
use strict;

この部分はほとんどおまじないです.標準出力に出すときにshiftjisに変換していますが,これは私の環境ではこうしないと文字を読めなかったからで,utf8をそのまま表示できる環境であれば,"use encoding 'utf-8' ..."の変わりに"use utf8;"としておいてください.

POE::Component::Jabber::Client::XMPP->new(
	IP => 'talk.google.com',
	PORT => '5222',
	HOSTNAME => 'gmail.com',
	USERNAME => 'whoami',
	PASSWORD => 'password',
	ALIAS => 'XMPP',
	STATE_PARENT => 'SESSION',
	STATES => {
		InitFinish => 'init_finished',
		InputEvent => 'input_event',
		ErrorEvent => 'error_event',
	}
)

ここの部分がGoogle Talkとつなぐための設定が書いてあるところです.IPは'talk.google.com', PORTは'5222'で固定なのですが,HOSTNAMEのところが日によって変わっているようです.今日は'gmail.com'で動いているのですが,一昨日はここを空欄にしないと'unknown host'というようなエラーメッセージが出て,サーバにアクセスできませんでした.USERNAME / PASSWORD はGmailで使っているものを設定します.

sub send_presence()
{
	my ($kernel, $show, $status) = @_[KERNEL, ARG0, ARG1];
	my ($node);
	$node = XNode->new('presence');
	$node->insert_tag('show')->data($show) if $show;
	$node->insert_tag('status')->data($status) if $status;
	$kernel->yield('output_event', $node);
}

認証に成功すると,POEは'init_finished'を呼び出し,'init_finished'が'send_presence'を呼び出します.認証に成功しただけでは,'Google Talk'上で'Available'にはなりません.プレゼンス情報をサーバに送る必要があります.一旦プレゼンス情報をサーバに送ると,フレンドのプレゼンス情報が送り返されてきます.

sub error_event()
{
	my ($kernel, $sender, $heap, $error) = @_[KERNEL, SENDER, HEAP, ARG0];
	if($error == +PCJ_SOCKFAIL) {
		my ($call, $code, $err) = @_[ARG1..ARG3];
		print "Socket error: $call, $code, $err\n";
	} elsif($error == +PCJ_SOCKDISC) {
		print "We got disconneted\n";
		print "Reconnecting!\n";
		$kernel->post($sender, 'reconnect_to_server');
	} elsif ($error == +PCJ_AUTHFAIL) {
		print "Failed to authenticate\n";
	} elsif ($error == +PCJ_BINDFAIL) {
		print "Failed to bind a resource\n";
	} elsif ($error == +PCJ_SESSFAIL) {
		print "Failed to establish a session\n";
	}
}
POE::Kernel->run();

一番多く出てくるエラーは'PCJ_SOCKDISC'です.TLS接続がうまくいかなくて,再接続を繰り返すことがあります.ちなみに,TLS環境も日によって設定が変わっているような感じです.このスクリプトでは5222番ポートでTLSを張っていますが,数日前まではTLSは5223番ポートでしか張ることができませんでした.これを解決するためには,POE::Component::Jabber::Clinet::XMPP

    538     tie
    539     (
    540         *$socket,
    541         'POE::Component::Jabber::Client::XMPP::TLS',
    542         $heap->{'socket'},
    543     ) or die $!;

となっているところで,'tie'の4番目の引数に5223を設定することで対処していました.つまり,こういうこと.

    538     tie
    539     (
    540         *$socket,
    541         'POE::Component::Jabber::Client::XMPP::TLS',
    542         $heap->{'socket'},
                5223,
    543     ) or die $!;

今は5222で繋がるので,上のスクリプトでなんとかなっているようです.そうそう,POE::Component::Jabber::Clinet::XMPPは'use strict'すると文法チェックでひっかかります.それを修正するには,

572c572
<       print STDERR "\n", scalar (localtime (time)), ": ". shift (@_) ."\n";
---
>       print STDERR "\n", scalar (localtime (time)), ": ". shift ."\n";

としてください.