File Coverage

blib/lib/WebService/LiveJournal/Client.pm
Criterion Covered Total %
statement 38 332 11.4
branch 0 128 0.0
condition 0 31 0.0
subroutine 13 51 25.4
pod 24 34 70.5
total 75 576 13.0


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