package AniDB::APIClient; use 5.024; use Moo; use experimental 'signatures'; use experimental 'postderef'; use IO::Async::Socket; use Log::Any '$log'; use curry::weak; use Encode; use namespace::clean; has [qw(username password)] => ( is => 'ro', required => 1, ); has client_name => ( is => 'ro', default => 'dakkarenamer', ); has client_version => ( is => 'ro', default => 1, ); has host => ( is => 'ro', default => 'api.anidb.info', ); has port => ( is => 'ro', default => 9000, ); has local_port => ( is => 'ro', default => 4568, ); sub login($self) { $self->_send( AUTH => { user => $self->username, pass => $self->password, protover => 3, client => $self->client_name, clientver => $self->client_version, nat => 1, enc => 'UTF-8', } )->then( sub($code,$payload) { my ($skey,$addr,$message) = split /\s+/,$payload,3; $log->debugf('login response: %s',$message); $self->_session_key($skey); return Future->done($message); } ); } sub logout($self) { $self->_send('LOGOUT')->then_done(); } has _loop => ( is => 'ro', init_arg => 'loop', default => sub { require IO::Async::Loop; return IO::Async::Loop->new; }, ); has _pending_requests => ( is => 'rw', default => sub { +{} }, ); sub _remove_pending_request($self,$tag,@) { delete $self->_pending_requests->{$tag}; } sub _future_for_tag($self,$tag) { my $f = $self->_loop->new_future; $f->on_done($self->curry::weak::_remove_pending_request($tag)); $self->_pending_requests->{$tag} = $f; return $f; } has _socket_f => ( is => 'lazy' ); sub _build__socket_f($self) { my $socket = IO::Async::Socket->new( on_recv => $self->curry::weak::_receive, ); $self->_loop->add($socket); return $socket->connect( host => $self->host, service => $self->port, socktype => 'dgram', ); } has _last_send_time => ( is => 'rw', default => 0 ); has _session_key => ( is => 'rw' ); sub _escape($str) { $str =~ s{&}{&}gr =~ s{\n}{
}gr } sub _send($self,$command,$command_args={},$delay=4,$want_reply=1) { if (!$self->_session_key && $command ne 'AUTH' && $command ne 'PING') { return $self->login ->then( $self->curry::weak::_send( $command, $command_args, $want_reply, $delay, ), ); } if ($delay && int(time - $self->_last_send_time) < $delay) { return $self->_loop->delay_future(after=>$delay) ->then( $self->curry::weak::_send( $command, $command_args, $want_reply, $delay, ), ); } $command_args->{tag} = my $tag = 'dr-'.(1+keys $self->_pending_requests->%*); $command_args->{s} = $self->_session_key if $self->_session_key; $log->debugf('command %s - %s',$command,$command_args); my $packet = "$command " . join '&', map { "$_=" . _escape( Encode::encode( 'UTF-8', $command_args->{$_}, Encode::FB_HTMLCREF | Encode::LEAVE_SRC, ) ) } sort keys $command_args->%*; my $reply_f = $want_reply ? $self->_future_for_tag($tag) : undef; my $send_f; $send_f = $self->_socket_f->then( sub($s) { $s->send($packet); $self->_last_send_time(time); undef $send_f; return Future->done; } ); return $reply_f; } sub _receive($self,$socket,$data,@) { $log->debugf('received packet <%s>',$data); my $tag; $data =~ s{\A(dr-\d+)\s+}{} and $tag=$1; unless ($tag) { $log->errorf('received untagged response <%s>',$data); return; } my $future = $self->_pending_requests->{$tag}; unless ($future) { $log->errorf( 'received unexpected response <%s> for tag <%s>', $data,$tag, ); return; } my ($code,$payload) = $data =~ m{\A(\d+)\s+(.*)\z}sm; $log->debugf( 'response for tag=%s code=%s payload=<%s>', $tag, $code, $payload, ); if ($code =~ /^2/) { $future->done($code,$payload); } else { $future->fail($code,$payload); } return; } 1;