File Coverage

blib/lib/WWW/Shorten/Bitly.pm
Criterion Covered Total %
statement 76 264 28.7
branch 13 122 10.6
condition 6 118 5.0
subroutine 21 35 60.0
pod 18 18 100.0
total 134 557 24.0


line stmt bran cond sub pod time code
1             package WWW::Shorten::Bitly;
2              
3 4     4   118273 use strict;
  4         4  
  4         89  
4 4     4   13 use warnings;
  4         3  
  4         71  
5 4     4   12 use Carp ();
  4         4  
  4         35  
6 4     4   1662 use File::HomeDir ();
  4         16598  
  4         63  
7 4     4   18 use File::Spec ();
  4         4  
  4         44  
8 4     4   1468 use JSON::MaybeXS;
  4         19162  
  4         203  
9 4     4   2675 use Path::Tiny qw(path);
  4         31394  
  4         199  
10 4     4   21 use Scalar::Util qw(blessed);
  4         4  
  4         228  
11 4     4   952 use URI ();
  4         6066  
  4         67  
12              
13 4     4   36 use base qw( WWW::Shorten::generic Exporter );
  4         4  
  4         1051  
14             our @EXPORT = qw(new version);
15              
16             our $VERSION = '2.000';
17             $VERSION = eval $VERSION;
18              
19 4   50 4   53428 use constant BASE_BLY => $ENV{BITLY_API_URL} || 'https://api-ssl.bitly.com';
  4         6  
  4         8075  
20              
21             # _attr (private)
22             sub _attr {
23 90     90   71 my $self = shift;
24 90   50     102 my $attr = lc(_trim(shift) || '');
25             # attribute list is small enough to just grep each time. meh.
26 90 50       70 Carp::croak("Invalid attribute") unless grep {$attr eq $_} @{_attrs()};
  450         441  
  90         97  
27 90 100       284 return $self->{$attr} unless @_;
28             # unset the access_token if any other field is set
29             # this ensures we're always connecting properly.
30 36         62 $self->{access_token} = undef;
31 36         23 my $val = shift;
32 36 50       55 unless (defined($val)) {
33 0         0 $self->{$attr} = undef;
34 0         0 return $self;
35             }
36 36         37 $self->{$attr} = $val;
37 36         52 return $self;
38             }
39              
40             # _attrs (static, private)
41             {
42             my $attrs; # mimic the state keyword
43             sub _attrs {
44 92 100   92   131 return [@{$attrs}] if $attrs;
  90         170  
45 2         7 $attrs = [
46             qw(username password access_token client_id client_secret),
47             ];
48 2         3 return [@{$attrs}];
  2         5  
49             }
50             }
51              
52             # _json_request (static, private)
53             sub _json_request {
54 0     0   0 my $url = shift;
55 0 0 0     0 Carp::croak("Invalid URI object") unless $url && blessed($url) && $url->isa('URI');
      0        
56 0         0 my $ua = __PACKAGE__->ua();
57 0         0 my $res = $ua->get($url);
58 0 0       0 Carp::croak("Invalid response") unless $res;
59 0 0       0 unless ($res->is_success) {
60 0         0 Carp::croak($res->status_line);
61             }
62              
63 0         0 my $content_type = $res->header('Content-Type');
64 0         0 my $content = $res->decoded_content();
65 0 0 0     0 unless ($content_type && $content_type =~ m{application/json}) {
66 0         0 Carp::croak("Unexpected response: $content");
67             }
68 0         0 my $json = decode_json($content);
69 0 0       0 Carp::croak("Invalid data returned: $content") unless $json;
70 0         0 return $json->{data};
71             }
72              
73             # _parse_args (static, private)
74             sub _parse_args {
75 0     0   0 my $args;
76 0 0 0     0 if ( @_ == 1 && ref $_[0] ) {
    0 0        
    0          
77 0         0 my %copy = eval { %{ $_[0] } }; # try shallow copy
  0         0  
  0         0  
78 0 0       0 Carp::croak("Argument to method could not be dereferenced as a hash") if $@;
79 0         0 $args = \%copy;
80             }
81             elsif (@_==1 && !ref($_[0])) {
82 0         0 $args = {single_arg => $_[0]};
83             }
84             elsif ( @_ % 2 == 0 ) {
85 0         0 $args = {@_};
86             }
87             else {
88 0         0 Carp::croak("Method got an odd number of elements");
89             }
90 0         0 return $args;
91             }
92              
93             # _parse_config (static, private)
94             {
95             my $config; # mimic the state keyword
96             sub _parse_config {
97             # always give back a shallow copy
98 2 50   2   5 return {%{$config}} if $config;
  0         0  
99             # only parse the file once, please.
100 2         3 $config = {};
101 2 50       10 my $file = $^O eq 'MSWin32'? '_bitly': '.bitly';
102 2 50       7 $file .= '_test' if $ENV{BITLY_TEST_CONFIG};
103 2         18 my $path = path(File::Spec->catfile(File::HomeDir->my_home(), $file));
104              
105 2 50 33     233 if ($path && $path->is_file) {
106 0         0 my @lines = $path->lines_utf8({chomp => 1});
107 0         0 my $attrs = _attrs();
108              
109 0         0 for my $line (@lines) {
110 0   0     0 $line = _trim($line) || '';
111 0 0       0 next if $line =~ /^\s*[;#]/; # skip comments
112 0         0 $line =~ s/\s+[;#].*$//gm; # trim off comments
113 0 0 0     0 next unless $line && $line =~ /=/; # make sure we have a =
114              
115 0         0 my ($key, $val) = split(/(?
116 0   0     0 $key = lc(_trim($key) || '');
117 0         0 $val = _trim($val);
118 0 0 0     0 next unless $key && $val;
119 0 0       0 $key = 'username' if $key eq 'user';
120 0 0       0 next unless grep {$key eq $_} @{$attrs};
  0         0  
  0         0  
121 0         0 $config->{$key} = $val;
122             }
123             }
124 2         106 return {%{$config}};
  2         16  
125             }
126             }
127              
128             # _trim (static, private)
129             sub _trim {
130 90     90   67 my $input = shift;
131 90 50 33     425 return $input unless defined $input && !ref($input) && length($input);
      33        
132 90         229 $input =~ s/\A\s*//;
133 90         250 $input =~ s/\s*\z//;
134 90         188 return $input;
135             }
136              
137             sub new {
138 2     2 1 23 my $class = shift;
139 2         3 my $args;
140 2 50 33     15 if ( @_ == 1 && ref $_[0] ) {
    50          
141 0         0 my %copy = eval { %{ $_[0] } }; # try shallow copy
  0         0  
  0         0  
142 0 0       0 Carp::croak("Argument to $class->new() could not be dereferenced as a hash") if $@;
143 0         0 $args = \%copy;
144             }
145             elsif ( @_ % 2 == 0 ) {
146 2         5 $args = {@_};
147             }
148             else {
149 0         0 Carp::croak("$class->new() got an odd number of elements");
150             }
151              
152 2         7 my $attrs = _attrs();
153             # start with what's in our config file (if anything)
154 2         6 my $href = _parse_config();
155             # override with anything passed in
156 2         5 for my $key (%{$args}) {
  2         5  
157 0         0 my $lc_key = lc($key);
158 0 0       0 $lc_key = 'username' if $lc_key eq 'user';
159 0 0       0 next unless grep {$lc_key eq $_} @{$attrs};
  0         0  
  0         0  
160 0         0 $href->{$lc_key} = $args->{$key};
161             }
162 2         8 return bless $href, $class;
163             }
164              
165 34     34 1 1948 sub access_token { return shift->_attr('access_token', @_); }
166              
167             sub bitly_pro_domain {
168 0     0 1 0 my $self = shift;
169 0 0       0 $self->login() unless ($self->access_token);
170              
171 0         0 my $args = _parse_args(@_);
172 0   0     0 my $link = $args->{domain} || $args->{url} || $args->{single_arg} || '';
173 0 0       0 unless ($link) {
174 0         0 Carp::croak("A domain parameter is required.\n");
175             }
176              
177 0         0 my $url = URI->new_abs('/v3/bitly_pro_domain', BASE_BLY);
178 0         0 $url->query_form(
179             access_token => $self->access_token(),
180             domain => $link,
181             format => 'json',
182             );
183 0         0 return _json_request($url);
184             }
185              
186 14     14 1 1325 sub client_id { return shift->_attr('client_id', @_); }
187              
188 14     14 1 1329 sub client_secret { return shift->_attr('client_secret', @_); }
189              
190             sub clicks {
191 0     0 1 0 my $self = shift;
192 0 0       0 $self->login() unless ($self->access_token);
193              
194 0         0 my $args = _parse_args(@_);
195 0   0     0 my $link = $args->{link} || $args->{single_arg} || '';
196 0 0       0 unless ($link) {
197 0         0 Carp::croak("A link parameter is required.\n");
198             }
199              
200 0         0 my $url = URI->new_abs('/v3/link/clicks', BASE_BLY);
201             $url->query_form(
202             access_token => $self->access_token(),
203             link => $link,
204             unit => $args->{unit} || 'day',
205             units => $args->{units} || '-1',
206             rollup => $args->{rollup}? 'true': 'false',
207             timezone => $args->{timezone} || 'America/New_York',
208             limit => $args->{limit} || 100,
209 0 0 0     0 unit_reference_ts => $args->{unit_reference_ts} || 'now',
      0        
      0        
      0        
      0        
210             format => 'json',
211             );
212 0         0 return _json_request($url);
213             }
214              
215             sub clicks_by_day {
216 0     0 1 0 my $self = shift;
217 0 0       0 $self->login() unless ($self->access_token);
218              
219 0         0 my $args = _parse_args(@_);
220 0   0     0 my $link = $args->{link} || $args->{single_arg} || '';
221 0 0       0 unless ($link) {
222 0         0 Carp::croak("A link parameter is required.\n");
223             }
224 0         0 $args->{unit} = 'day';
225 0         0 $args->{units} = 7;
226 0         0 $args->{link} = $link;
227 0         0 return $self->clicks($args);
228             }
229              
230             sub countries {
231 0     0 1 0 my $self = shift;
232 0 0       0 $self->login() unless ($self->access_token);
233              
234 0         0 my $args = _parse_args(@_);
235 0   0     0 my $link = $args->{link} || $args->{single_arg} || '';
236 0 0       0 unless ($link) {
237 0         0 Carp::croak("A link parameter is required.\n");
238             }
239              
240 0         0 my $url = URI->new_abs('/v3/link/countries', BASE_BLY);
241             $url->query_form(
242             access_token => $self->access_token(),
243             link => $link,
244             unit => $args->{unit} || 'day',
245             units => $args->{units} || '-1',
246             timezone => $args->{timezone} || 'America/New_York',
247             limit => $args->{limit} || 100,
248 0   0     0 unit_reference_ts => $args->{unit_reference_ts} || 'now',
      0        
      0        
      0        
      0        
249             format => 'json',
250             );
251 0         0 return _json_request($url);
252             }
253              
254             sub expand {
255 0     0 1 0 my $self = shift;
256 0 0       0 $self->login() unless ($self->access_token);
257              
258 0         0 my $args = _parse_args(@_);
259 0   0     0 my $short_url = $args->{shortUrl} || $args->{URL} || $args->{url} || $args->{single_arg} || '';
260 0 0       0 unless ($short_url) {
261 0         0 Carp::croak("A shortUrl parameter is required.\n");
262             }
263              
264 0         0 my $url = URI->new_abs('/v3/expand', BASE_BLY);
265             $url->query_form(
266             access_token => $self->access_token(),
267             shortUrl => $short_url,
268             hash => $args->{hash},
269 0         0 format => 'json',
270             );
271 0         0 return _json_request($url);
272             }
273              
274             sub info {
275 0     0 1 0 my $self = shift;
276 0 0       0 $self->login() unless ($self->access_token);
277 0         0 my $args = _parse_args(@_);
278              
279 0   0     0 my $link = $args->{shortUrl} || $args->{single_arg} || '';
280 0 0       0 unless ($link) {
281 0         0 Carp::croak("A shortUrl parameter is required.\n");
282             }
283              
284 0         0 my $url = URI->new_abs('/v3/info', BASE_BLY);
285             $url->query_form(
286             access_token => $self->access_token(),
287             shortUrl => $link,
288             hash => $args->{hash},
289 0 0       0 expand_user => $args->{expand_user}? 'true': 'false',
290             format => 'json',
291             );
292 0         0 return _json_request($url);
293             }
294              
295             sub login {
296 0     0 1 0 my $self = shift;
297 0 0       0 return $self if $self->{access_token};
298              
299 0         0 my $username = $self->{username};
300 0         0 my $password = $self->{password};
301 0         0 my $id = $self->{client_id};
302 0         0 my $secret = $self->{client_secret};
303 0         0 my $url = URI->new_abs('/oauth/access_token', BASE_BLY);
304 0 0 0     0 unless ($username && $password) {
305 0         0 Carp::croak("Can't login without at least a username and password");
306             }
307 0         0 my $req = HTTP::Request->new(POST => $url);
308 0         0 $req->header(Accept => 'application/json');
309 0 0 0     0 if ($id && $secret) {
310 0         0 $req->authorization_basic($id,$secret);
311 0         0 my $content = URI->new();
312 0         0 $content->query_form(
313             grant_type=>'password',
314             username=>$username,
315             password=>$password,
316             );
317 0         0 $req->content($content->query());
318             }
319             else {
320 0         0 $req->authorization_basic($username,$password);
321             }
322 0         0 my $ua = __PACKAGE__->ua();
323 0         0 my $res = $ua->request($req);
324 0 0       0 Carp::croak("Invalid response") unless $res;
325 0 0       0 unless ($res->is_success) {
326 0         0 Carp::croak($res->status_line);
327             }
328              
329 0         0 my $content_type = $res->header('Content-Type');
330 0         0 my $content = $res->decoded_content();
331 0 0 0     0 if ($content_type && $content_type =~ m{application/json}) {
332 0         0 my $json = decode_json($res->decoded_content());
333 0 0       0 Carp::croak("Invalid data returned") unless $json;
334 0 0       0 Carp::croak($content) unless ($json->{access_token});
335 0         0 $content = $json->{access_token};
336             }
337 0         0 $self->access_token($content);
338 0         0 return $self;
339             }
340              
341             sub lookup {
342 0     0 1 0 my $self = shift;
343 0 0       0 $self->login() unless ($self->access_token);
344 0         0 my $args = _parse_args(@_);
345              
346 0   0     0 my $link = $args->{url} || $args->{single_arg} || '';
347 0 0       0 unless ($link) {
348 0         0 Carp::croak("A url parameter is required.\n");
349             }
350              
351 0         0 my $url = URI->new_abs('/v3/link/lookup', BASE_BLY);
352 0         0 $url->query_form(
353             access_token => $self->access_token(),
354             link => $link,
355             format => 'json',
356             );
357 0         0 return _json_request($url);
358             }
359              
360             sub makeashorterlink {
361 0     0 1 0 my $self;
362 0 0 0     0 if ($_[0] && blessed($_[0]) && $_[0]->isa('WWW::Shorten::Bitly')) {
      0        
363 0         0 $self = shift;
364             }
365 0 0       0 my $url = shift or Carp::croak('No URL passed to makeashorterlink');
366 0   0     0 $self ||= __PACKAGE__->new(@_);
367 0         0 my $res = $self->shorten(longUrl=>$url, @_);
368 0         0 return $res->{url};
369             }
370              
371             sub makealongerlink {
372 0     0 1 0 my $self;
373 0 0 0     0 if ($_[0] && blessed($_[0]) && $_[0]->isa('WWW::Shorten::Bitly')) {
      0        
374 0         0 $self = shift;
375             }
376 0 0       0 my $url = shift or Carp::croak('No URL passed to makealongerlink');
377 0   0     0 $self ||= __PACKAGE__->new(@_);
378 0         0 my $res = $self->expand(shortUrl=>$url, @_);
379 0 0       0 return '' unless ref($res->{expand}) eq 'ARRAY';
380 0         0 for my $row (@{$res->{expand}}) {
  0         0  
381 0         0 return $row->{long_url};
382             }
383 0         0 return '';
384             }
385              
386 14     14 1 1294 sub password { return shift->_attr('password', @_); }
387              
388             sub referrers {
389 0     0 1 0 my $self = shift;
390 0 0       0 $self->login() unless ($self->access_token);
391 0         0 my $args = _parse_args(@_);
392              
393 0   0     0 my $link = $args->{link} || $args->{single_arg} || '';
394 0 0       0 unless ($link) {
395 0         0 Carp::croak("A link parameter is required.\n");
396             }
397              
398 0         0 my $url = URI->new_abs('/v3/link/referrers', BASE_BLY);
399             $url->query_form(
400             access_token => $self->access_token(),
401             link => $link,
402             unit => $args->{unit} || 'day',
403             units => $args->{units} || -1,
404             timezone => $args->{timezone} || 'America/New_York',
405             limit => $args->{limit} || 100,
406 0   0     0 unit_reference_ts => $args->{unit_reference_ts} || 'now',
      0        
      0        
      0        
      0        
407             format => 'json',
408             );
409 0         0 return _json_request($url);
410             }
411              
412             sub shorten {
413 0     0 1 0 my $self = shift;
414 0 0       0 $self->login() unless ($self->access_token);
415 0         0 my $args = _parse_args(@_);
416              
417 0   0     0 my $long_url = $args->{longUrl} || $args->{single_arg} || $args->{URL} || $args->{url} || '';
418 0   0     0 my $domain = $args->{domain} || undef;
419 0 0       0 unless ($long_url) {
420 0         0 Carp::croak("A longUrl parameter is required.\n");
421             }
422              
423 0         0 my $url = URI->new_abs('/v3/shorten', BASE_BLY);
424 0         0 $url->query_form(
425             access_token => $self->access_token(),
426             longUrl => $long_url,
427             domain => $domain,
428             format => 'json',
429             );
430 0         0 return _json_request($url);
431             }
432              
433 14     14 1 1301 sub username { return shift->_attr('username', @_); }
434              
435              
436             1; # End of WWW::Shorten::Bitly
437             __END__