File Coverage

blib/lib/WebService/LiveJournal/Client.pm
Criterion Covered Total %
statement 16 18 88.8
branch n/a
condition n/a
subroutine 6 6 100.0
pod n/a
total 22 24 91.6


line stmt bran cond sub pod time code
1             package WebService::LiveJournal::Client;
2              
3 12     12   69072 use strict;
  12         23  
  12         406  
4 12     12   62 use warnings;
  12         20  
  12         287  
5 12     12   135 use v5.10;
  12         35  
  12         647  
6 12     12   20185 use overload '""' => \&as_string;
  12         12165  
  12         95  
7 12     12   738 use Digest::MD5 qw(md5_hex);
  12         21  
  12         808  
8 12     12   19327 use RPC::XML;
  0            
  0            
9             use RPC::XML::Client;
10             use WebService::LiveJournal::FriendList;
11             use WebService::LiveJournal::FriendGroupList;
12             use WebService::LiveJournal::Event;
13             use WebService::LiveJournal::EventList;
14             use WebService::LiveJournal::Tag;
15             use HTTP::Cookies;
16              
17             # ABSTRACT: Interface to the LiveJournal API
18             our $VERSION = '0.07'; # VERSION
19              
20              
21             my $zero = new RPC::XML::int(0);
22             my $one = new RPC::XML::int(1);
23             our $lineendings_unix = new RPC::XML::string('unix');
24             my $challenge = new RPC::XML::string('challenge');
25             our $error;
26             our $error_request;
27              
28             $RPC::XML::ENCODING = 'utf-8'; # uh... and WHY??? is this a global???
29              
30              
31             sub new # arg: server, port, username, password, mode
32             {
33             my $ob = shift;
34             my $class = ref($ob) || $ob;
35             my $self = bless {}, $class;
36            
37             my %arg = @_;
38            
39             my $server = $self->{server} = $arg{server} // 'www.livejournal.com';
40             my $domain = $server;
41             $domain =~ s/^([A-Za-z0-9]+)//;
42             $self->{domain} = $domain;
43             my $port = $self->{port} = $arg{port} || 80;
44             $server .= ":$port" if $port != 80;
45             my $client = $self->{client} = new RPC::XML::Client("http://$server/interface/xmlrpc");
46             $self->{flat_url} = "http://$server/interface/flat";
47             my $cookie_jar = $self->{cookie_jar} = new HTTP::Cookies;
48             $client->useragent->cookie_jar($cookie_jar);
49             $client->useragent->default_headers->push_header('X-LJ-Auth' => 'cookie');
50              
51             $self->{mode} = $arg{mode} // 'cookie'; # can be cookie or challenge
52              
53             my $username = $self->{username} = $arg{username};
54             my $password = $arg{password};
55             $self->{password} = $password if $self->{mode} ne 'cookie';
56            
57             $self->{auth} = [ ver => $one ];
58             $self->{flat_auth} = [ ver => 1 ];
59            
60             if($self->{mode} eq 'cookie')
61             {
62            
63             my $response = $self->send_request('getchallenge');
64             return unless defined $response;
65             my $auth_challenge = $response->value->{challenge};
66             my $auth_response = md5_hex($auth_challenge, md5_hex($password));
67            
68             push @{ $self->{auth} }, username => new RPC::XML::string($username);
69             push @{ $self->{flat_auth} }, user => $username;
70              
71             $response = $self->send_request('sessiongenerate',
72             auth_method => $challenge,
73             auth_challenge => new RPC::XML::string($auth_challenge),
74             auth_response => new RPC::XML::string($auth_response),
75             );
76              
77             return unless defined $response;
78              
79             my $ljsession = $self->{ljsession} = $response->value->{ljsession};
80             $self->set_cookie(ljsession => $ljsession);
81             push @{ $self->{auth} }, auth_method => new RPC::XML::string('cookie');
82             push @{ $self->{flat_auth} }, auth_method => 'cookie';
83            
84             }
85             elsif($self->{mode} eq 'challenge')
86             {
87             push @{ $self->{auth} }, username => new RPC::XML::string($username);
88             push @{ $self->{flat_auth} }, user => $username;
89             }
90              
91             my $response = $self->send_request('login'
92             #getmoods => $zero,
93             #getmenus => $one,
94             #getpickws => $one,
95             #getpickwurls => $one,
96             );
97            
98             return unless defined $response;
99            
100             my $h = $response->value;
101             return $self->_set_error($h->{faultString}) if defined $h->{faultString};
102             return $self->_set_error("unknown LJ error " . $h->{faultCode}->value) if defined $h->{faultCode};
103            
104             $self->{userid} = $h->{userid};
105             $self->{fullname} = $h->{fullname};
106             $self->{usejournals} = $h->{usejournals} || [];
107             my $fastserver = $self->{fastserver} = $h->{fastserver};
108            
109             if($fastserver)
110             {
111             $self->set_cookie(ljfastserver => 1);
112             }
113            
114             if($h->{friendgroups})
115             {
116             my $fg = $self->{cachefriendgroups} = new WebService::LiveJournal::FriendGroupList(response => $response);
117             }
118            
119             $self->{message} = $h->{message};
120             return $self;
121             }
122              
123              
124             foreach my $name (qw( server username port userid fullname usejournals fastserver cachefriendgroups message cookie_jar ))
125             {
126             eval qq{ sub $name { shift->{$name} } };
127             die $@ if $@;
128             }
129              
130             sub useragent { $_[0]->{client}->useragent }
131              
132              
133             sub create_event
134             {
135             my $self = shift;
136             my $event = new WebService::LiveJournal::Event(client => $self, @_);
137             $event;
138             }
139              
140             # legacy
141             sub create { shift->create_event(@_) }
142              
143              
144             sub get_events
145             {
146             my $self = shift;
147             my @list;
148             my $selecttype = shift || 'lastn';
149             push @list, selecttype => new RPC::XML::string($selecttype);
150              
151             my %arg = @_;
152              
153             if($selecttype eq 'syncitems')
154             {
155             push @list, lastsync => new RPC::XML::string($arg{lastsync}) if defined $arg{lastsync};
156             }
157             elsif($selecttype eq 'day')
158             {
159             unless(defined $arg{day} && defined $arg{month} && defined $arg{year})
160             {
161             return $self->_set_error('attempt to use selecttype=day without providing day!');
162             }
163             push @list,
164             day => new RPC::XML::int($arg{day}),
165             month => new RPC::XML::int($arg{month}),
166             year => new RPC::XML::int($arg{year});
167             }
168             elsif($selecttype eq 'lastn')
169             {
170             push @list, howmany => new RPC::XML::int($arg{howmany}) if defined $arg{howmany};
171             push @list, howmany => new RPC::XML::int($arg{max}) if defined $arg{max};
172             push @list, beforedate => new RPC::XML::string($arg{beforedate}) if defined $arg{beforedate};
173             }
174             elsif($selecttype eq 'one')
175             {
176             my $itemid = $arg{itemid} || -1;
177             push @list, itemid => new RPC::XML::int($itemid);
178             }
179             else
180             {
181             return $self->_set_error("unknown selecttype: $selecttype");
182             }
183            
184             push @list, truncate => new RPC::XML::int($arg{truncate}) if $arg{truncate};
185             push @list, prefersubject => $one if $arg{prefersubject};
186             push @list, lineendings => $lineendings_unix;
187             push @list, usejournal => RPX::XML::string($arg{usejournal}) if $arg{usejournal};
188             push @list, usejournal => RPX::XML::string($arg{journal}) if $arg{journal};
189              
190             my $response = $self->send_request('getevents', @list);
191             return unless defined $response;
192             if($selecttype eq 'one')
193             {
194             return new WebService::LiveJournal::Event(client => $self, %{ $response->value->{events}->[0] });
195             }
196             else
197             {
198             return new WebService::LiveJournal::EventList(client => $self, response => $response);
199             }
200             }
201              
202             # legacy
203             sub getevents { shift->get_events(@_) }
204              
205              
206             sub get_event
207             {
208             my $self = shift;
209             my %args = @_ == 1 ? (itemid => shift) : (@_);
210             $self->get_events('one', %args);
211             }
212              
213             # legacy
214             sub getevent { shift->get_event(@_) }
215              
216              
217             sub sync_items
218             {
219             my $self = shift;
220             my $cb = sub {};
221             $cb = pop if ref($_[-1]) eq 'CODE';
222             my %arg = @_;
223            
224             my $return_time;
225            
226             my @req_args = ();
227             if(defined $arg{last_sync})
228             {
229             @req_args = ( lastsync => $arg{last_sync} );
230             $return_time = $arg{last_sync};
231             }
232            
233             eval {
234             while(1)
235             {
236             my $response = $self->send_request('syncitems', @req_args);
237             last unless defined $response;
238             my $count = $response->value->{count};
239             my $total = $response->value->{total};
240             foreach my $item (@{ $response->value->{syncitems} })
241             {
242             unless($item->{item} =~ /^(.)-(\d+)$/)
243             {
244             die 'internal error: ' . $item->{item} . ' does not match';
245             }
246             $cb->($item->{action}, $1, $2);
247             $return_time = $item->{time};
248             }
249             last if $count == $total;
250             @req_args = ( lastsync => $arg{return_time} );
251             };
252             };
253             $WebService::LiveJournal::Client::error = $@ if $@;
254             return $return_time;
255             }
256              
257              
258             sub get_friends
259             {
260             my $self = shift;
261             my %arg = @_;
262             my @list;
263             push @list, friendlimit => new RPC::XML::int($arg{friendlimit}) if defined $arg{friendlimit};
264             push @list, friendlimit => new RPC::XML::int($arg{limit}) if defined $arg{limit};
265             push @list, includefriendof => 1, includegroups => 1 if $arg{complete};
266             my $response = $self->send_request('getfriends', @list);
267             return unless defined $response;
268             if($arg{complete})
269             {
270             return (new WebService::LiveJournal::FriendList(response_list => $response->value->{friends}),
271             new WebService::LiveJournal::FriendList(response_list => $response->value->{friendofs}),
272             new WebService::LiveJournal::FriendGroupList(response => $response),
273             );
274             }
275             else
276             {
277             return new WebService::LiveJournal::FriendList(response => $response);
278             }
279             }
280              
281             sub getfriends { shift->get_friends(@_) }
282              
283              
284             sub get_friend_of
285             {
286             my $self = shift;
287             my %arg = @_;
288             my @list;
289             push @list, friendoflimit => new RPC::XML::int($arg{friendoflimit}) if defined $arg{friendoflimit};
290             push @list, friendoflimit => new RPC::XML::int($arg{limit}) if defined $arg{limit};
291             my $response = $self->send_request('friendof', @list);
292             return unless defined $response;
293             return new WebService::LiveJournal::FriendList(response => $response);
294             }
295              
296             sub friendof { shift->get_friend_of(@_) }
297              
298              
299             sub get_friend_groups
300             {
301             my $self = shift;
302             my $response = $self->send_request('getfriendgroups');
303             return unless defined $response;
304             return new WebService::LiveJournal::FriendGroupList(response => $response);
305             }
306              
307             sub getfriendgroups { shift->get_friend_groups(@_) }
308              
309              
310             sub get_user_tags
311             {
312             my($self, $journal_name) = @_;
313             my @request = ('getusertags');
314             push @request, usejournal => RPC::XML::string->new($journal_name)
315             if defined $journal_name;
316             my $response = $self->send_request(@request);
317             return unless defined $response;
318             return map { WebService::LiveJournal::Tag->new($_) } @{ $response->value->{tags} };
319             }
320              
321              
322             sub console_command
323             {
324             my $self = shift;
325            
326             my $response = $self->send_request('consolecommand',
327             commands => RPC::XML::array->new(
328             RPC::XML::array->new(
329             map { RPC::XML::string->new($_) } @_
330             ),
331             ),
332             );
333             return unless defined $response;
334             return $response->value->{results}->[0]->{output};
335             }
336              
337              
338             sub batch_console_commands
339             {
340             my $self = shift;
341             my @commands;
342             my @cb;
343             for(0..$#_)
344             {
345             if($_ % 2)
346             { push @cb, $_[$_] }
347             else
348             { push @commands, RPC::XML::array->new(map { RPC::XML::string->new($_) } @{ $_[$_] }) }
349             }
350            
351             my $response = $self->send_request('consolecommand',
352             commands => RPC::XML::array->new(@commands)
353             );
354             return unless defined $response;
355              
356             # also returned is 'success' but as far as I can tell it is always
357             # 1, even if the command doesn't exist. so we are ignoring it.
358            
359             foreach my $output (map { $_->{output} } @{ $response->value->{results} })
360             {
361             my $cb = shift @cb;
362             $cb->(@$output);
363             }
364            
365             return 1;
366             }
367              
368              
369             sub set_cookie
370             {
371             my $self = shift;
372             my $key = shift;
373             my $value = shift;
374              
375             $self->cookie_jar->set_cookie(
376             0, # version
377             $key => $value, # key => value
378             '/', # path
379             $self->{domain}, # domain
380             $self->port, # port
381             1, # path_spec
382             0, # secure
383             60*60*24, # maxage
384             0, # discard
385             );
386             }
387              
388              
389             sub send_request
390             {
391             my $self = shift;
392             $self->_clear_error;
393             my $count = $self->{count} || 1;
394             my $procname = shift;
395            
396             my @challenge;
397             if($self->{mode} eq 'challenge')
398             {
399             my $response = $self->{client}->send_request('LJ.XMLRPC.getchallenge');
400             if(ref $response)
401             {
402             if($response->is_fault)
403             {
404             my $string = $response->value->{faultString};
405             my $code = $response->value->{faultCode};
406             $self->_set_error("$string ($code) on LJ.XMLRPC.getchallenge");
407             return;
408             }
409             # else, stuff worked fall through
410             }
411             else
412             {
413             if($count < 5 && $response =~ /HTTP server error: Method Not Allowed/i)
414             {
415             $self->{count} = $count+1;
416             print STDERR "retry ($count)\n";
417             sleep 10;
418             my $response = $self->send_request($procname, @_);
419             $self->{count} = $count;
420             return $response;
421             }
422             return $self->_set_error($response);
423             }
424              
425             # this is where we fall through down to from above
426             my $auth_challenge = $response->value->{challenge};
427             my $auth_response = md5_hex($auth_challenge, md5_hex($self->{password}));
428             @challenge = (
429             auth_method => $challenge,
430             auth_challenge => new RPC::XML::string($auth_challenge),
431             auth_response => new RPC::XML::string($auth_response),
432             );
433             }
434              
435             my $request = new RPC::XML::request(
436             "LJ.XMLRPC.$procname",
437             new RPC::XML::struct(
438             @{ $self->{auth} },
439             @challenge,
440             @_,
441             ),
442             );
443              
444             #use Test::More;
445             #use XML::LibXML;
446             #use XML::LibXML::PrettyPrint;
447             #my $xml = XML::LibXML->new->parse_string($request->as_string);
448             #my $pp = XML::LibXML::PrettyPrint->new(indent_string => ' ')->pretty_print($xml);
449             #note 'send request:';
450             #note $xml->toString;
451              
452             my $response = $self->{client}->send_request($request);
453            
454             #my $xml = XML::LibXML->new->parse_string($response->as_string);
455             #my $pp = XML::LibXML::PrettyPrint->new(indent_string => ' ')->pretty_print($xml);
456             #note 'recv response:';
457             #note $xml->toString;
458            
459             if(ref $response)
460             {
461             if($response->is_fault)
462             {
463             my $string = $response->value->{faultString};
464             my $code = $response->value->{faultCode};
465             $self->_set_error("$string ($code) on LJ.XMLRPC.$procname");
466             $error_request = $request;
467             return;
468             }
469             return $response;
470             }
471             else
472             {
473             if($count < 5 && $response =~ /HTTP server error: Method Not Allowed/i)
474             {
475             $self->{count} = $count+1;
476             print STDERR "retry ($count)\n";
477             sleep 10;
478             my $response = $self->send_request($procname, @_);
479             $self->{count} = $count;
480             return $response;
481             }
482             return $self->_set_error($response);
483             }
484             }
485              
486             sub _post
487             {
488             my $self = shift;
489             my $ua = $self->{client}->useragent;
490             my %arg = @_;
491             #use Test::More;
492             #note "====\nOUT:\n";
493             #foreach my $key (keys %arg)
494             #{
495             # note "$key=$arg{$key}\n";
496             #}
497             my $http_response = $ua->post($self->{flat_url}, \@_);
498             return $self->_set_error("HTTP Error: " . $http_response->status_line) unless $http_response->is_success;
499            
500             my $response_text = $http_response->content;
501             my @list = split /\n/, $response_text;
502             my %h;
503             #note "====\nIN:\n";
504             while(@list > 0)
505             {
506             my $key = shift @list;
507             my $value = shift @list;
508             #note "$key=$value\n";
509             $h{$key} = $value;
510             }
511            
512             return $self->_set_error("LJ Protocol error, server didn't return a success value") unless defined $h{success};
513             return $self->_set_error("LJ Protocol error: $h{errmsg}") if $h{success} ne 'OK';
514            
515             return \%h;
516             }
517              
518             sub as_string
519             {
520             my $self = shift;
521             my $username = $self->username;
522             my $server = $self->server;
523             "[ljclient $username\@$server]";
524             }
525              
526             sub findallitemid
527             {
528             my $self = shift;
529             my %arg = @_;
530             my $response = $self->send_request('syncitems');
531             die $error unless defined $response;
532             my $count = $response->value->{count};
533             my $total = $response->value->{total};
534             my $time;
535             my @list;
536             while(1)
537             {
538             #print "$count/$total\n";
539             foreach my $item (@{ $response->value->{syncitems} })
540             {
541             $time = $item->{time};
542             my $id = $item->{item};
543             my $action = $item->{action};
544             if($id =~ /^L-(\d+)$/)
545             {
546             push @list, $1;
547             }
548             }
549            
550             last if $count == $total;
551              
552             $response = $self->send_request('syncitems', lastsync => $time);
553             die $error unless defined $response;
554             $count = $response->value->{count};
555             $total = $response->value->{total};
556             }
557              
558             return @list;
559             }
560              
561              
562             sub send_flat_request
563             {
564             my $self = shift;
565             $self->_clear_error;
566             my $count = $self->{count} || 1;
567             my $procname = shift;
568             my $ua = $self->{client}->useragent;
569              
570             my @challenge;
571             if($self->{mode} eq 'challenge')
572             {
573             my $h = _post($self, mode => 'getchallenge');
574             return unless defined $h;
575             my %h = %{ $h };
576              
577             my $auth_challenge = $h{challenge};
578              
579             my $auth_response = md5_hex($auth_challenge, md5_hex($self->{password}));
580             @challenge = (
581             auth_method => 'challenge',
582             auth_challenge => $auth_challenge,
583             auth_response => $auth_response,
584             );
585             }
586            
587             return _post($self,
588             mode => $procname,
589             @{ $self->{flat_auth} },
590             @challenge,
591             @_
592             );
593             }
594              
595             sub _set_error
596             {
597             my($self, $value) = @_;
598             $error = $value;
599             return;
600             }
601              
602             sub _clear_error
603             {
604             undef $error;
605             }
606              
607              
608             sub error { $error }
609              
610             1;
611              
612             __END__