File Coverage

blib/lib/WebService/Blekko.pm
Criterion Covered Total %
statement 105 211 49.7
branch 21 78 26.9
condition 15 43 34.8
subroutine 19 28 67.8
pod 11 15 73.3
total 171 375 45.6


line stmt bran cond sub pod time code
1             #
2              
3             package WebService::Blekko;
4              
5             =head1 NAME
6              
7             WebService::Blekko - access the Blekko JSON APIs
8              
9             =cut
10              
11 3     3   145415 use strict;
  3         11  
  3         187  
12 3     3   18 use warnings;
  3         6  
  3         101  
13 3     3   15 no warnings qw( uninitialized );
  3         9  
  3         112  
14              
15 3     3   3197 use LWP::UserAgent;
  3         210167  
  3         120  
16 3     3   35 use LWP::Protocol;
  3         6  
  3         70  
17 3     3   17 use HTTP::Request;
  3         7  
  3         86  
18 3     3   18 use List::Util qw( min );
  3         7  
  3         418  
19 3     3   1094 use Time::HiRes;
  3         2118  
  3         29  
20 3     3   4134 use JSON;
  3         43497  
  3         16  
21              
22 3     3   2756 use Data::Dumper;
  3         15551  
  3         223  
23              
24 3     3   2067 use WebService::Blekko::QueryResultSet;
  3         10  
  3         99  
25 3     3   1805 use WebService::Blekko::Pagestats;
  3         13  
  3         96  
26 3     3   1775 use WebService::Blekko::Result;
  3         9  
  3         9654  
27              
28             our $VERSION = '1.00_07';
29              
30             my $useragent = __PACKAGE__ . '_' . $VERSION;
31              
32             =head1 SYNOPSIS
33              
34             use WebService::Blekko;
35              
36             my $blekko = WebService::Blekko->new( auth => 'webservice-blekko-example', );
37              
38             $res = $blekko->query( "obama /date" );
39              
40             if ( $res->error ) { ... }
41              
42             while ( my $r = $res->next ) {
43             print $r->url, $r->title; # etc.
44             }
45              
46             =head1 DESCRIPTION
47              
48             This API wraps the Blekko search engine API(s). You can query for results,
49             manipulate slashtags, get tool-bar-useful information, and so forth.
50              
51             For the Terms and Conditions for using Blekko data, please see
52              
53             https://blekko.com/ws/+/terms
54             and
55             https://blekko.com/ws/+/apiterms
56              
57             To get an API Auth key, please contact apiauth@blekko.com
58              
59             =head1 METHODS
60              
61             =head2 new( %opts )
62              
63             Options include
64              
65             server => server to talk to, defaults to blekko.com
66             auth => api auth key, gotten by contacting apiauth@blekko.com
67             source => the name of your program/service
68             page_size => number of results to return, default 20, max of 100
69             scheme => http, defaults to https if available
70             qps => API calls per second, defaults to 1. Do not make this greater than 1 without asking.
71             agent => the user-agent to be used by LWP::UserAgent. Defaults to the package name_version.
72              
73             cookie_jar_file => cookie jar file to use, see LWP::UserAgent
74             cookie_jar => cookie jar object to use, see LWP::UserAgent
75              
76             Additional options are passed to LWP::UserAgent.
77              
78             =cut
79              
80             sub new
81             {
82 5     5 1 578 my $class = shift;
83 5         16 my $self = bless {}, $class;
84              
85 5         27 my %opts = @_;
86              
87 5         27 my $https_available = LWP::Protocol::implementor( 'https' );
88              
89 5   100     2721 $self->{server} = delete $opts{server} || 'blekko.com';
90 5   50     27 $self->{auth} = delete $opts{auth} || die "Must specify auth in opts";
91 5         22 $self->{auth} = "&auth=" . urlencode( $self->{auth} );
92 5   33     38 $self->{source} = delete $opts{source} || $useragent;
93 5         13 $self->{source} = "&source=" . urlencode( $self->{source} );
94 5         18 $self->{page_size} = delete $opts{page_size};
95 5   66     31 $self->{scheme} = delete $opts{scheme} || ( $https_available ? 'https' : 'http' );
96 5   100     70 $self->{qps} = min( delete $opts{qps} || 1, 1 );
97 5         10 $self->{last_query} = 0;
98              
99 5   33     31 $opts{agent} = $opts{agent} || $useragent;
100 5         14 my $cjf = delete $opts{cookie_jar_file};
101 5         18 my $cj = delete $opts{cookie_jar};
102 5   50     31 $opts{max_redirect} ||= 0; # don't follow redirects
103              
104             # is the scheme valid?
105 5 50       20 die "invalid scheme $self->{scheme}" if ! LWP::Protocol::implementor($self->{scheme});
106              
107             # remaining opts are for LWP::UserAgent... default timeout is 180 seconds, yuck.
108 5         139206 $self->{ua} = LWP::UserAgent->new( %opts );
109 5 50       14044 return if ! defined $self->{ua};
110              
111 5 50       19 $self->{ua}->cookie_jar( { file => $cjf }, autosave => 1, ) if defined $cjf;
112 5 50       17 $self->{ua}->cookie_jar( $cj ) if defined $cj;
113              
114 5 50 33     37 $self->{have_cookie_jar} = 1 if defined $cjf || defined $cj;
115              
116 5         29 return $self;
117             }
118              
119             =head2 query( query_string, %opts )
120              
121             Queries the server, and returns a WebService::Blekko::QueryResultSet.
122              
123             Options include page_size (see above) and p, to get the pth page of
124             results, counting from zero.
125              
126             =cut
127              
128             sub query
129             {
130 8     8 1 4277 my ( $self, $q, %opts ) = @_;
131              
132 8         19 my $template = "%s://%s/ws/?q=%s%s%s%s%s";
133              
134 8         14 my $ps = '';
135 8 100       52 $ps = "&page_size=" . urlencode( $self->{page_size} ) if $self->{page_size};
136 8 100       27 $ps = "&page_size=" . urlencode( $opts{page_size} ) if $opts{page_size};
137 8         17 my $p = '';
138 8 100       23 $p = "&p=" . urlencode( $opts{p} ) if $opts{p};
139              
140 8         44 my $url = sprintf( $template, $self->{scheme}, $self->{server},
141             urlencode( "/json $q" ), $self->{auth}, $self->{source}, $ps, $p );
142              
143 8         95 my $req = HTTP::Request->new( 'GET', $url );
144 8         11969 $self->query_sleep();
145 8         75 my $resp = $self->{ua}->request( $req );
146              
147 8         5400884 return WebService::Blekko::QueryResultSet->new( $resp->content, $resp->code );
148             }
149              
150             =head2 pagestats( url )
151              
152             Returns information about a webpage, suitable for toolbar use. Returns
153             a WebService::Blekko::Pagestats object, with methods host_inlinks, host_rank, etc.
154              
155             =cut
156              
157             sub pagestats
158             {
159 3     3 1 455 my ( $self, $url ) = @_;
160              
161 3 100       29 if ( $url !~ m,^https?://,i )
162             {
163 1         16 return WebService::Blekko::Pagestats->new( undef, "url must start with http://", 200 );
164             }
165              
166 2         5 my $template = "%s://%s/api/pagestats?url=%s%s%s";
167 2         27 $url = sprintf( $template, $self->{scheme}, $self->{server}, $url, $self->{auth}, $self->{source} );
168              
169 2         21 my $req = HTTP::Request->new( 'GET', $url );
170 2         12825 $self->query_sleep();
171 2         24 my $resp = $self->{ua}->request( $req );
172              
173 2 50       830062 if ( ! $resp->is_success )
174             {
175 0         0 return WebService::Blekko::Pagestats->new( undef, "http failure, code is ".$resp->code, $resp->code );
176             }
177              
178 2         49 return WebService::Blekko::Pagestats->new( $resp->content, 0, $resp->code );
179             }
180              
181             =head2 login( username, password )
182              
183             Logs into blekko, which is needed before you create/add to/delete slashtags. Requires
184             a cookie jar file or object to work.
185              
186             Returns WebService::Blekko::Result, which has methods error, result,
187             and http_code. Check error before using result.
188              
189             =cut
190              
191             sub login
192             {
193 0     0 1 0 my ( $self, $username, $password ) = @_; # opts
194              
195 0 0       0 if ( ! $self->{have_cookie_jar} )
196             {
197 0         0 return WebService::Blekko::Result->new( 0, "No cookie jar configured. Read the WebServer::Blekko docs.", 200 );
198             }
199              
200 0         0 my $template = "https://%s/login?u=%s&p=%s%s%s"; # forced to https
201 0         0 my $url = sprintf( $template, $self->{server}, $username, $password, $self->{auth}, $self->{source} );
202              
203 0         0 my $req = HTTP::Request->new( 'GET', $url );
204 0         0 $self->query_sleep();
205 0         0 my $resp = $self->{ua}->request( $req );
206              
207 0 0       0 if ( ! $resp->is_success )
208             {
209 0         0 return WebService::Blekko::Result->new( '', "http failure, code is ".$resp->code, $resp->code );
210             }
211              
212 0         0 my $answer = my_decode_json( $resp->content ); # XXX does this need an eval?
213              
214 0 0 0     0 if ( defined $answer->{status} && $answer->{status} )
215             {
216 0         0 return WebService::Blekko::Result->new( 1, 0, $resp->code );
217             }
218              
219 0         0 return WebService::Blekko::Result->new( 0, 'Login failed', $resp->code );
220             }
221              
222             =head2 logout()
223              
224             Logs out of blekko. Does not throw an error if you are already logged out.
225              
226             =cut
227              
228             sub logout
229             {
230 0     0 1 0 my ( $self ) = @_;
231              
232 0         0 my $url = "$self->{scheme}://$self->{server}/logout";
233              
234 0         0 my $req = HTTP::Request->new( 'GET', $url );
235              
236 0         0 $self->query_sleep();
237 0         0 my $resp = $self->{ua}->request( $req );
238              
239             # redir is success
240 0 0       0 if ( $resp->is_redirect )
241             {
242 0         0 return WebService::Blekko::Result->new( 1, 0, $resp->code );
243             }
244              
245 0         0 return WebService::Blekko::Result->new( 0, 'Logout failed', $resp->code );
246             }
247              
248             =head2 user_info()
249              
250             Returns the username of the currently logged-in user. Useful in
251             toolbars, where the user logs directly into blekko.
252              
253             =cut
254              
255             sub user_info
256             {
257 0     0 1 0 my ( $self ) = @_;
258              
259 0         0 my $url = "$self->{scheme}://$self->{server}/api/userinfo";
260              
261 0         0 my $req = HTTP::Request->new( 'GET', $url );
262              
263 0         0 $self->query_sleep();
264 0         0 my $resp = $self->{ua}->request( $req );
265              
266 0 0       0 if ( ! $resp->is_success )
267             {
268 0         0 return WebService::Blekko::Result->new( '', "http failure, code is ".$resp->code, $resp->code );
269             }
270              
271 0         0 my $answer = my_decode_json( $resp->content ); # XXX does this need an eval?
272              
273 0 0 0     0 if ( defined $answer->{username} && $answer->{username} )
274             {
275 0         0 return WebService::Blekko::Result->new( $answer->{username}, 0, $resp->code );
276             }
277              
278 0         0 return WebService::Blekko::Result->new( 0, 'Login failed', $resp->code );
279             }
280              
281             =head2 create_slashtag( $slashtag, \@urls, $description )
282              
283             Creates a slashtag.
284              
285             =cut
286              
287             sub create_slashtag
288             {
289 0     0 1 0 my $self = shift;
290 0         0 return $self->createupdate( "create", @_ );
291             }
292              
293             =head2 add_urls( $slashtag, \@urls )
294              
295             Adds urls to an existing slashtag.
296              
297             =cut
298              
299             sub add_urls
300             {
301 0     0 1 0 my $self = shift;
302 0         0 return $self->createupdate( "update", @_, undef );
303             }
304              
305             sub createupdate
306             {
307 0     0 0 0 my ( $self, $createupdate, $slashtag, $urls, $desc ) = @_; # XXX opts... at least urls is an array ref...
308              
309 0 0 0     0 if ( defined $urls && ref $urls ne 'ARRAY' )
310             {
311 0         0 return WebService::Blekko::Result->new( 0, "\$urls must be an array ref", 200 );
312             }
313              
314 0         0 my $urls_string = '';
315 0 0       0 if ( @$urls )
316             {
317 0         0 @$urls = map { urlencode( $_ ); } @$urls;
  0         0  
318 0         0 $urls_string = "&urls=" . join( '%0A', @$urls );
319             }
320 0 0       0 $desc = $desc ? "&desc=" . urlencode( $desc ) : '';
321              
322 0         0 my $template = "%s://%s/tag/add?name=%s&submit=%s%s%s%s%s";
323 0         0 my $url = sprintf( $template, $self->{scheme}, $self->{server}, $slashtag, $createupdate,
324             $urls_string, $desc, $self->{auth}, $self->{source} );
325              
326 0         0 my $req = HTTP::Request->new( 'GET', $url );
327 0         0 $self->query_sleep();
328 0         0 my $resp = $self->{ua}->request( $req );
329              
330 0 0       0 if ( $resp->is_redirect )
331             {
332 0 0       0 if ( $createupdate eq "create" )
333             {
334 0         0 return WebService::Blekko::Result->new( 0, "You are not logged in, or tag already exists", 200 );
335             }
336             else
337             {
338 0         0 return WebService::Blekko::Result->new( 0, "You are not logged in", 200 );
339             }
340             }
341              
342 0 0       0 if ( ! $resp->is_success )
343             {
344 0         0 return WebService::Blekko::Result->new( 0, "http failure, code is ".$resp->code, $resp->code );
345             }
346              
347 0 0       0 if ( $resp->content ne '' )
348             {
349 0         0 return WebService::Blekko::Result->new( 0, "Error: ".$resp->content, $resp->code );
350             }
351              
352 0         0 return WebService::Blekko::Result->new( 1, 0, $resp->code );
353             }
354              
355             =head2 list_urls( $slashtag )
356              
357             Returns an arrayref of the urls in the slashtag
358              
359             =cut
360              
361             sub list_urls
362             {
363 0     0 1 0 my ( $self, $slashtag ) = @_; # XXX opts
364              
365 0         0 my $template = "%s://%s/tag/view?name=%s&format=text%s%s";
366 0         0 my $url = sprintf( $template, $self->{scheme}, $self->{server}, $slashtag, $self->{auth}, $self->{soure} );
367              
368 0         0 my $req = HTTP::Request->new( 'GET', $url );
369 0         0 $self->query_sleep();
370 0         0 my $resp = $self->{ua}->request( $req );
371              
372 0 0       0 if ( ! $resp->is_success )
373             {
374 0         0 return WebService::Blekko::Result->new( 0, "http failure, code is ".$resp->code, $resp->code );
375             }
376              
377             # if error, html is returned, even though we said 'format=text'
378             # future proofed by also considering 'Error:' to indicate an error
379 0 0 0     0 if ( substr( $resp->content, 0, 1 ) eq '<' || substr( $resp->content, 0, 6 ) eq 'Error:' )
380             {
381 0         0 return WebService::Blekko::Result->new( 0, "No such slashtag or other error", $resp->code );
382             }
383              
384 0         0 my @answer = split /\n/, $resp->content;
385              
386 0         0 return WebService::Blekko::Result->new( \@answer, 0, $resp->code );
387             }
388              
389             =head2 delete_urls( $slashtag, \@urls )
390              
391             Deletes urls in a slashtag.
392              
393             =cut
394              
395             # XXX also &tags= to delete subtags in a slashtag
396              
397             sub delete_urls
398             {
399 0     0 1 0 my ( $self, $slashtag, $urls ) = @_; # XXX opts
400              
401 0 0 0     0 if ( defined $urls && ref $urls ne 'ARRAY' )
402             {
403 0         0 return WebService::Blekko::Result->new( 0, "\$urls must be an array ref", 200 );
404             }
405              
406 0         0 my $urls_string = '';
407 0 0       0 if ( @$urls )
408             {
409 0         0 @$urls = map { urlencode( $_ ); } @$urls;
  0         0  
410 0         0 $urls_string = "&urls=" . join( '%0A', @$urls );
411             }
412              
413 0         0 my $template = "%s://%s/tag/edit?submit=1&type=del&name=%s%s%s%s";
414 0         0 my $url = sprintf( $template, $self->{scheme}, $self->{server}, $slashtag, $urls_string, $self->{auth}, $self->{source} );
415              
416 0         0 my $req = HTTP::Request->new( 'GET', $url );
417 0         0 $self->query_sleep();
418 0         0 my $resp = $self->{ua}->request( $req );
419              
420 0 0       0 if ( ! $resp->is_success )
421             {
422 0         0 return WebService::Blekko::Result->new( 0, "http failure, code is ".$resp->code, $resp->code );
423             }
424              
425             # this always returns javascript :-/ so key off css
426             # XXX future-proof me
427 0 0       0 if ( $resp->content =~ /alertMsgError/ )
428             {
429 0         0 return WebService::Blekko::Result->new( 0, "No such slashtag or other error", $resp->code );
430             }
431              
432 0         0 return WebService::Blekko::Result->new( 1, 0, $resp->code );
433             }
434              
435             =head2 remove_slashtag( $slashtag )
436              
437             Removes a slashtag.
438              
439             =cut
440              
441             sub remove_slashtag
442             {
443 0     0 1 0 my ( $self, $slashtag ) = @_; # XXX opts
444              
445 0         0 my $template = "%s://%s/tag/delete?submit=1&name=%s%s%s";
446 0         0 my $url = sprintf( $template, $self->{scheme}, $self->{server}, $slashtag, $self->{auth}, $self->{source} );
447              
448 0         0 my $req = HTTP::Request->new( 'GET', $url );
449 0         0 $self->query_sleep();
450 0         0 my $resp = $self->{ua}->request( $req );
451              
452 0 0       0 if ( ! $resp->is_success )
453             {
454 0         0 return WebService::Blekko::Result->new( 0, "http failure, code is ".$resp->code, $resp->code );
455             }
456              
457 0 0       0 if ( $resp->content =~ /alertMsgErr/ )
458             {
459 0         0 return WebService::Blekko::Result->new( 0, "Error", $resp->code );
460             }
461              
462 0 0       0 if ( $resp->content =~ / has been deleted/ )
463             {
464 0         0 return WebService::Blekko::Result->new( 1, 0, $resp->code );
465             }
466              
467 0         0 return WebService::Blekko::Result->new( 0, "Did not see success", $resp->code );
468             }
469              
470             # poor man's request rate limiter
471              
472             sub query_sleep
473             {
474 10     10 0 27 my ( $self ) = @_;
475              
476 10         55 my $now = Time::HiRes::time;
477 10   50     105 my $delta = 1. / ( $self->{qps} || 1 );
478              
479 10 100       59 if ( $now - $self->{last_query} < $delta )
480             {
481 3         11 my $s = $self->{last_query} + $delta - $now;
482 3         1250108 Time::HiRes::sleep( $s );
483 3         65 $self->{last_query} = Time::HiRes::time;
484             }
485             else
486             {
487 7         21 $self->{last_query} = $now;
488             }
489             }
490              
491             # ----------------------------------------------------------------------
492             # to go away
493             # ----------------------------------------------------------------------
494              
495             my %escapes;
496             for (0..255)
497             {
498             $escapes{chr($_)} = sprintf("%%%02X", $_);
499             }
500             $escapes{' '} = '+';
501              
502             sub urlencode
503             {
504 25     25 0 49 my $url = shift;
505              
506 25 50       166 $url =~ s/([^A-Za-z0-9\-_.!~*\'()])/$escapes{$1}/ge if defined $url;
  29         165  
507 25         186 return $url;
508             }
509              
510             # keep the JSON::Boolean bs down to a minimum -- why isn't this an option in JSON?
511             # I can't be the only person using JSON for non-roundtrip purposes.
512             sub my_decode_json
513             {
514 8     8 0 21 my ( $string ) = @_;
515              
516 8 100 66     84 return if ! defined $string || $string eq '';
517              
518 7         1100 my $ret = decode_json( $string ); # XXX needs eval?
519              
520 7 50 33     69 if ( $ret && ref $ret eq 'HASH' )
521             {
522 7         43 foreach my $k ( keys %$ret )
523             {
524 81         128 my $it = $ret->{$k};
525 81 50       335 if ( UNIVERSAL::isa( $it, 'JSON::Boolean' ) )
526             {
527 0 0       0 $ret->{$k} = 1 if $it eq $JSON::true;
528 0 0       0 $ret->{$k} = 0 if $it eq $JSON::false;
529             }
530             }
531             }
532 7         39 return $ret;
533             }
534              
535             =head1 SEE ALSO
536              
537             L, L
538              
539             =head1 AUTHOR
540              
541             "Greg Lindahl", Egreg@blekko.comE
542              
543             Thanks to Fred Moyer for commenting on the interfaces.
544              
545             =head1 COPYRIGHT AND LICENSE
546              
547             Copyright (C) 2011 by blekko, inc.
548              
549             This library is free software; you can redistribute it and/or modify
550             it under the same terms as Perl itself, either Perl version 5.12.3 or,
551             at your option, any later version of Perl 5 you may have available.
552              
553             =cut
554              
555             1;
556