{ package Catalyst::Engine::XMPP2; use strict; use warnings; our $VERSION = '0.3'; use base qw(Catalyst::Engine::Embeddable); use Event qw(loop); use Encode; use HTTP::Request; use Net::XMPP2::Connection; use UNIVERSAL qw(isa); __PACKAGE__->mk_accessors(qw( connections )); my %http_xmpp_error_map = ( 400 => { cond => 'bad-request', type => 'modify' }, 409 => { cond => 'conflict', type => 'cancel' }, 501 => { cond => 'feature-not-implemented', type => 'cancel' }, 403 => { cond => 'forbidden', type => 'auth' }, 410 => { cond => 'gone', type => 'modify' }, 500 => { cond => 'internal-server-error', type => 'wait' }, 404 => { cond => 'item-not-found', type => 'cancel' }, 520 => { cond => 'jid-malformed', type => 'modify' }, 406 => { cond => 'not-acceptable', type => 'modify' }, 420 => { cond => 'not-allowed', type => 'cancel' }, 401 => { cond => 'not-authorized', type => 'auth' }, 402 => { cond => 'payment-required', type => 'auth' }, 521 => { cond => 'recipient-unavailable', type => 'wait' }, 302 => { cond => 'redirect', type => 'modify' }, 421 => { cond => 'registration-required', type => 'auth' }, 502 => { cond => 'remote-server-not-found', type => 'cancel' }, 504 => { cond => 'remote-server-timeout', type => 'wait' }, 412 => { cond => 'resource-constraint', type => 'wait' }, 503 => { cond => 'service-unavailable', type => 'cancel' }, 422 => { cond => 'subscription-required', type => 'auth' }, 423 => { cond => 'undefined-condition', type => 'cancel' }, 424 => { cond => 'unexpected-request', type => 'wait' }, ); sub run { my ($self, $app) = @_; die 'No Engine::XMPP2 configuration found' unless ref $app->config->{'Engine::XMPP2'} eq 'HASH'; # list the path actions that will be mapped as resources. my %uniq_ns; my @resources = map { s#^/?## ; $_ } map { @{$_->attributes->{Path} || []} } values %{$app->dispatcher->action_hash}; $self->connections({}) unless $self->connections(); my %template = %{$app->config->{'Engine::XMPP2'}}; delete $template{jid}; delete $template{resource}; #$app->log->debug('Initializing Net::XMPP2::Connection objects'); foreach my $resource (@resources) { $self->connections->{$resource} = Net::XMPP2::Connection->new(resource => $resource, %template); } #$app->log->debug('Connecting XMPP resources.'); foreach my $resource (@resources) { $self->connections->{$resource}->connect or die 'Could not connect resource: '.$resource.', '.$!; $self->connections->{$resource}->reg_cb (stream_ready => sub { $self->connections->{$resource}->send_presence('available', sub{}); }, bind_error => sub { die 'Error binding resource '.$resource.': '.shift; }, # the four events are registered as separate to let Net::XMPP2 # handle all other types of events, but we can actually process # them the same way. iq_get_request_xml => sub { my ($conn, $node) = @_; #$app->log->debug('Received an iq get stanza at '.$resource); $self->handle_xmpp_node($app, $resource, $node, 'iq'); }, iq_set_request_xml => sub { my ($conn, $node) = @_; #$app->log->debug('Received an iq set stanza at '.$resource); $self->handle_xmpp_node($app, $resource, $node, 'iq'); }, message_xml => sub { my ($conn, $node) = @_; #$app->log->debug('Received a message stanza at '.$resource); $self->handle_xmpp_node($app, $resource, $node, 'message'); }, presence_xml => sub { my ($conn, $node) = @_; #$app->log->debug('Received a presence stanza at '.$resource); $self->handle_xmpp_node($app, $resource, $node, 'presence'); }); } loop(); } sub handle_xmpp_node { my ($self, $app, $resource, $node, $type) = @_; my $config = $app->config->{'Engine::XMPP2'}; my $url = 'xmpp://'.$config->{username}.'@'.$config->{domain}.'/'.$resource; my $request = HTTP::Request->new(POST => $url); $request->header('Content-type' => 'application/xml; charset=utf-8'); $request->header('XMPP_Stanza' => $type); $request->header('XMPP_Resource' => $resource); $request->header('XMPP_Stanza_'.$_ => $node->attr($_)) for grep { $node->attr($_) } qw(to from id type xml:lang); my $content = join '', $node->text, map { $_->as_string } $node->nodes; $request->content_length( length($content) ); $request->content( $content); #$app->log->debug('[Request Content] '.$request->content); my $response; $app->handle_request($request, \$response); my %response_attrs = map { $_ => $response->header('XMPP_Stanza_'.$_) } grep { $response->header($_) } qw(to from id type xml:lang); if ($response->is_success && $type ne 'iq') { #$app->log->debug('Request ended successfully, no response needed.'); } elsif ($response->is_success) { my $content_type = $response->header('Content-type'); my $content_raw = $response->content(); $self->connections->{$resource}->reply_iq_result ($node, sub { my $xml_writer = shift; my $ctype = $content_type; my $craw = $content_raw; if ($ctype && $ctype =~ /xml/) { $xml_writer->raw($craw); } else { $xml_writer->raw('
'.$craw.''); } }, %response_attrs); } else { my $cond = $http_xmpp_error_map{$response->code}{cond} || 'internal-server-error'; my $type = $http_xmpp_error_map{$response->code}{type} || 'wait'; if (my $over = $response->header('XMPP_error-type')) { $type = $over; } if ($node->name eq 'iq') { $self->connections->{$resource}->reply_iq_error ($node, $type, $cond, %response_attrs); } else { my $content_raw = $response->content(); $self->connections->{$resource}->send_message ($node->attr('from'), 'error', sub { my $xml_writer = shift; $xml_writer->raw($content.'