File Coverage

blib/lib/Net/Twitter/Core.pm
Criterion Covered Total %
statement 144 150 96.0
branch 41 50 82.0
condition 25 41 60.9
subroutine 38 40 95.0
pod 0 2 0.0
total 248 283 87.6


line stmt bran cond sub pod time code
1             package Net::Twitter::Core;
2             $Net::Twitter::Core::VERSION = '4.01042';
3             # ABSTRACT: A perl interface to the Twitter API
4              
5 41     41   1643 use 5.008001;
  40         93  
6 40     33   228 use Moose;
  33         41  
  33         180  
7 33     33   140748 use Carp::Clan qw/^(?:Net::Twitter|Moose|Class::MOP)/;
  33         1393  
  33         210  
8 33     33   5594 use JSON::MaybeXS;
  33         1002  
  33         1495  
9 33     33   9922 use URI::Escape;
  33         24215  
  33         1612  
10 33     33   14321 use HTTP::Request::Common;
  33         385797  
  33         2361  
11 33     33   13667 use Net::Twitter::Error;
  33         114  
  33         1829  
12 33     33   273 use Scalar::Util qw/blessed reftype/;
  33         42  
  33         2224  
13 33     33   134 use List::Util qw/first/;
  33         40  
  33         1735  
14 33     33   19433 use HTML::Entities ();
  33         150125  
  33         1244  
15 33     33   14877 use Encode qw/encode_utf8/;
  33         234758  
  33         2140  
16 33     33   21956 use DateTime;
  33         9904690  
  33         1505  
17 33     33   19623 use Data::Visitor::Callback;
  33         1483703  
  33         1376  
18 33     33   239 use Try::Tiny;
  33         48  
  33         2120  
19              
20 33     33   151 use namespace::autoclean;
  33         45  
  33         288  
21              
22             has useragent_class => ( isa => 'Str', is => 'ro', default => 'LWP::UserAgent' );
23             has useragent_args => ( isa => 'HashRef', is => 'ro', default => sub { {} } );
24             has username => ( isa => 'Str', is => 'rw', predicate => 'has_username' );
25             has password => ( isa => 'Str', is => 'rw', predicate => 'has_password' );
26             has ssl => ( isa => 'Bool', is => 'ro', default => 0 );
27             has netrc => ( isa => 'Str', is => 'ro', predicate => 'has_netrc' );
28             has netrc_machine => ( isa => 'Str', is => 'ro', default => 'api.twitter.com' );
29             has decode_html_entities => ( isa => 'Bool', is => 'rw', default => 0 );
30              
31             has useragent => (
32                 isa => 'Str',
33                 is => 'ro',
34                 default => "Net::Twitter/${ \($Net::Twitter::Core::VERSION || 1) } (Perl)",
35             );
36              
37             has source => ( isa => 'Str', is => 'ro', default => 'twitterpm' );
38             has ua => ( isa => 'Object', is => 'rw', lazy => 1, builder => '_build_ua' );
39             has clientname => ( isa => 'Str', is => 'ro', default => 'Perl Net::Twitter' );
40              
41             has clientver => (
42                 is => 'ro',
43                 isa => 'Str',
44                 default => $Net::Twitter::Core::VERSION || 1,
45             );
46              
47             has clienturl => ( isa => 'Str', is => 'ro', default => 'http://search.cpan.org/dist/Net-Twitter/' );
48             has _base_url => ( is => 'rw' ); ### keeps role composition from bitching ??
49             has _json_handler => (
50                 is => 'rw',
51                 default => sub { JSON->new->allow_nonref->utf8 },
52                 handles => { from_json => 'decode' },
53             );
54              
55 668     668   2533 sub _legacy_synthetic_args { qw/authenticate since/ }
56              
57             sub _remap_legacy_synthetic_args {
58 668     668   808     my ( $self, $args ) = @_;
59              
60 668         1245     $args->{"-$_"} = delete $args->{$_} for grep exists $args->{$_}, $self->_legacy_synthetic_args;
61             }
62              
63             sub _natural_args {
64 667     667   551     my ( $self, $args ) = @_;
65              
66 667         2110     map { $_ => $args->{$_} } grep !/^-/, keys %$args;
  314         941  
67             }
68              
69             around BUILDARGS => sub {
70                 my $next = shift;
71                 my $class = shift;
72              
73                 my %options = @_ == 1 ? %{$_[0]} : @_;
74              
75             # Default to ssl
76                 $options{ssl} = 1 unless exists $options{ssl};
77              
78             # aliases
79                 for ( [ user => 'username' ], [ pass => 'password' ] ) {
80                     my ( $alias, $base ) = @$_;
81                     if ( exists $options{$alias} ) {
82                         if ( !defined $options{$base} ) {
83                             $options{$base} = delete $options{$alias};
84                         }
85                         else {
86                             carp "Both $base and $alias provided. Ignoring $alias";
87                         }
88                     }
89                 }
90              
91                 if ( delete $options{identica} ) {
92                     %options = (
93                         apiurl => 'http://identi.ca/api',
94                         searchapiurl => 'http://identi.ca/api',
95                         apirealm => 'Laconica API',
96                         oauth_urls => {
97                             request_token_url => "https://identi.ca/api/oauth/request_token",
98                             authentication_url => "https://identi.ca/api/oauth/authenticate",
99                             authorization_url => "https://identi.ca/api/oauth/authorize",
100                             access_token_url => "https://identi.ca/api/oauth/access_token",
101                             xauth_url => "https://identi.ca/api/oauth/access_token",
102                         },
103                         %options,
104                     );
105                 }
106              
107                 return $next->($class, \%options);
108             };
109              
110             sub BUILD {
111 113     113 0 3904     my $self = shift;
112              
113 113 100       2967     if ( $self->has_netrc ) {
114 2         11         require Net::Netrc;
115              
116             # accepts '1' for backwards compatibility
117 2 50       45         my $host = $self->netrc eq '1' ? $self->netrc_machine : $self->netrc;
118 2   33     11         my $nrc = Net::Netrc->lookup($host)
119                         || croak "No .netrc entry for $host";
120              
121 2         325         my ($user, $pass) = $nrc->lpa;
122 2         57         $self->username($user);
123 2         48         $self->password($pass);
124                 }
125              
126 113 100       3029     $self->credentials($self->username, $self->password) if $self->has_username;
127             }
128              
129             sub _build_ua {
130 33     33   52     my $self = shift;
131              
132 33     25   914     eval "use " . $self->useragent_class;
  25         5594  
  25         120681  
  25         566  
133 33 100       157     croak $@ if $@;
134              
135 32         980     my $ua = $self->useragent_class->new(%{$self->useragent_args});
  32         894  
136 32         60680     $ua->agent($self->useragent);
137 32         2540     $ua->default_header('X-Twitter-Client' => $self->clientname);
138 32         2249     $ua->default_header('X-Twitter-Client-Version' => $self->clientver);
139 32         1997     $ua->default_header('X-Twitter-Client-URL' => $self->clienturl);
140 32         1176     $ua->env_proxy;
141              
142 32         73537     return $ua;
143             }
144              
145             sub credentials {
146 45     45 0 914     my ($self, $username, $password) = @_;
147              
148 45         1008     $self->username($username);
149 45         938     $self->password($password);
150              
151 45         507     return $self; # make it chainable
152             }
153              
154             sub _encode_args {
155 667     667   605     my ($self, $args) = @_;
156              
157             # Values need to be utf-8 encoded. Because of a perl bug, exposed when
158             # client code does "use utf8", keys must also be encoded.
159             # see: http://www.perlmonks.org/?node_id=668987
160             # and: http://perl5.git.perl.org/perl.git/commit/eaf7a4d2
161 667 100       934     return { map { utf8::upgrade($_) unless ref($_); $_ } %$args };
  628         1466  
  628         1037  
162             }
163              
164             sub _json_request {
165 667     667   1504     my ($self, $http_method, $uri, $args, $authenticate, $dt_parser, $content_type ) = @_;
166              
167 667         1209     my $msg = $self->_prepare_request($http_method, $uri, $args, $authenticate, $content_type);
168 667         1253     my $res = $self->_send_request($msg);
169              
170 666         378236     return $self->_parse_result($res, $args, $dt_parser);
171             }
172              
173             sub _prepare_request {
174 667     667   791     my ($self, $http_method, $uri, $args, $authenticate, $content_type ) = @_;
175              
176 667         517     my $msg;
177              
178 667         1224     my %natural_args = $self->_natural_args($args);
179              
180 667         1172     $self->_encode_args(\%natural_args);
181 667 50       2810     if( $http_method eq 'PUT' ) {
    100          
    50          
182 0         0         $msg = PUT(
183                         $uri,
184                         'Content-Type' => 'application/x-www-form-urlencoded',
185                         Content => $self->_query_string_for( \%natural_args ) );
186                 }
187                 elsif ( $http_method =~ /^(?:GET|DELETE)$/ ) {
188 421         744         $uri->query($self->_query_string_for(\%natural_args));
189 421         7287         $msg = HTTP::Request->new($http_method, $uri);
190                 }
191                 elsif ( $http_method eq 'POST' ) {
192 246 50 33     546         if( $content_type && $content_type eq 'application/json' ) {
193 0         0             $msg = POST( $uri, Content_Type => 'application/json', Content => encode_json \%natural_args );
194                     }
195                     else {
196             # if any of the arguments are (array) refs, use form-data
197 174     174   386             $msg = (first { ref } values %natural_args)
198                             ? POST($uri,
199                                    Content_Type => 'form-data',
200                                    Content => [
201 246 100       1463                            map { ref $_ ? $_ : encode_utf8 $_ } %natural_args,
  6 100       36  
202                                    ],
203                             )
204                             : POST($uri, Content => $self->_query_string_for(\%natural_args))
205                             ;
206                     }
207                 }
208                 else {
209 0         0         croak "unexpected HTTP method: $http_method";
210                 }
211              
212 667 100       55179     $self->_add_authorization_header($msg, \%natural_args) if $authenticate;
213              
214 667         40965     return $msg;
215             }
216              
217             # Make sure we encode arguments *exactly* the same way Net::OAuth does
218             # ...by letting Net::OAuth encode them.
219             sub _query_string_for {
220 665     665   582     my ( $self, $args ) = @_;
221              
222 665         576     my @pairs;
223 665         1688     while ( my ($k, $v) = each %$args ) {
224 311         6614         push @pairs, join '=', map URI::Escape::uri_escape_utf8($_,'^\w.~-'), $k, $v;
225                 }
226              
227 665         14119     return join '&', @pairs;
228             }
229              
230             # Basic Auth, overridden by Role::OAuth, if included
231             sub _add_authorization_header {
232 641     641   708     my ( $self, $msg ) = @_;
233              
234 641 100 66     19373     $msg->headers->authorization_basic($self->username, $self->password)
235                     if $self->has_username && $self->has_password;
236             }
237              
238 672     672   15554 sub _send_request { shift->ua->request(shift) }
239              
240             has _decode_html_entities_visitor => (
241                 is => 'rw',
242                 lazy => 1,
243                 default => sub {
244                     Data::Visitor::Callback->new(
245                         plain_value => sub {
246                             return unless defined $_;
247              
248                             $_ = HTML::Entities::decode_entities($_);
249                         }
250                     )
251                 },
252             );
253              
254 0     0   0 sub _decode_html_entities { shift->_decode_html_entities_visitor->visit(@_) }
255              
256             # By default, Net::Twitter does not inflate objects, so just return the
257             # hashref, untouched. This is really just a hook for Role::InflateObjects.
258 659     659   590 sub _inflate_objects { return $_[2] }
259              
260             sub _parse_result {
261 666     666   846     my ($self, $res, $args, $datetime_parser) = @_;
262              
263             # workaround for Laconica API returning bools as strings
264             # (Fixed in Laconi.ca 0.7.4)
265 666         1226     my $content = $res->content;
266 666         4932     $content =~ s/^"(true|false)"$/$1/;
267              
268 666 100   664   3763     my $obj = length $content ? try { $self->from_json($content) } : {};
  664         11934  
269 666 50 66     28310     $self->_decode_html_entities($obj) if $obj && $self->decode_html_entities;
270              
271             # filter before inflating objects
272 666 100 66     2057     if ( (my $since = delete $args->{-since}) && defined $obj ) {
273 5         61         $self->_filter_since($datetime_parser, $obj, $since);
274                 }
275              
276             # inflate the twitter object(s) if possible
277 665         5733     $self->_inflate_objects($datetime_parser, $obj);
278              
279             # Twitter sometimes returns an error with status code 200
280 665 100 100     12222     if ( ref $obj && reftype $obj eq 'HASH' && (exists $obj->{error} || exists $obj->{errors}) ) {
      66        
      66        
281 5         160         die Net::Twitter::Error->new(twitter_error => $obj, http_response => $res);
282                 }
283              
284 660 100 100     1478     return $obj if $res->is_success && defined $obj;
285              
286 5 100       224     die Net::Twitter::Error->new(
287                     http_response => $res,
288                     $obj ? ( twitter_error => $obj ) : (),
289                 );
290             }
291              
292             # Return a DateTime object, given $since as one of:
293             # - DateTime object
294             # - string in format "YYYY-MM-DD"
295             # - string in the same format as created_at values for the particular
296             # Twitter API (Search and REST have different created_at formats!)
297             # - an integer with epoch time (in seconds)
298             # Otherwise, throw an exception
299             sub _since_as_datetime {
300 5     5   5     my ($self, $since, $parser) = @_;
301              
302 5 100 66     29     return $since if blessed($since) && $since->isa('DateTime');
303              
304 4 100       14     if ( my ($y, $m, $d) = $since =~ /^(\d{4})-(\d{2})-(\d{2})$/ ) {
305 1         10         return DateTime->new(month => $m, day => $d, year => $y);
306                 }
307              
308                 return eval { DateTime->from_epoch(epoch => $since) }
309 3   66     3         || eval { $parser->parse_datetime($since) }
310                     || croak
311             "Invalid 'since' parameter: $since. Must be a DateTime, epoch, string in Twitter timestamp format, or YYYY-MM-DD.";
312             }
313              
314             sub _filter_since {
315 5     5   8     my ($self, $datetime_parser, $obj, $since) = @_;
316              
317             # $since can be a DateTime, an epoch value, or a Twitter formatted timestamp
318 5         16     my $since_dt = $self->_since_as_datetime($since, $datetime_parser);
319              
320                 my $visitor = Data::Visitor::Callback->new(
321                     ignore_return_values => 1,
322                     array => sub {
323 3     3   420             my ($visitor, $data) = @_;
324              
325 3 50       11             return unless $self->_contains_statuses($data);
326              
327             # truncate $data when we reach an item as old or older than $since_dt
328 3         4             my $i = 0;
329 3         8             while ( $i < @$data ) {
330 6 100       21                 last if $datetime_parser->parse_datetime($data->[$i]{created_at}) <= $since_dt;
331 3         1731                 ++$i;
332                         }
333 3         1526             $#{$data} = $i - 1;
  3         17  
334                     }
335 4         1256     );
336              
337 4         520     $visitor->visit($obj);
338             }
339              
340             # check an arrayref to see if it contains statuses
341             sub _contains_statuses {
342 3     3   3     my ($self, $arrayref) = @_;
343              
344 3   50     9     my $e = $arrayref->[0] || return;
345 3 50 33     17     return unless ref $e && reftype $e eq 'HASH';
346 3   33     16     return exists $e->{created_at} && exists $e->{text} && exists $e->{id};
347             }
348              
349             sub _user_or_undef {
350 4     4   11     my ( $self, $orig, $type, @rest ) = @_;
351              
352                 return try {
353 4     4   101         $orig->($self, @rest);
354                 }
355                 catch {
356 0 0   0   0         die $_ unless /The specified user is not a $type of this list/;
357 0         0         undef;
358 4         23     };
359             }
360              
361             1;
362              
363             __END__
364            
365             =for Pod::Coverage BUILD credentials
366            
367             =head1 NAME
368            
369             Net::Twitter::Core - Net::Twitter implementation
370            
371             =head1 VERSION
372            
373             version 4.01042
374            
375             =head1 SYNOPSIS
376            
377             use Net::Twitter::Core;
378            
379             my $nt = Net::Twitter::Core->new_with_traits(traits => [qw/API::Search/]);
380            
381             my $tweets = $nt->search('perl twitter')
382            
383             =head1 DESCRIPTION
384            
385             This module implements the core features of C<Net::Twitter>. See L<Net::Twitter> for full documentation.
386            
387             Although this module can be used directly, you are encouraged to use C<Net::Twitter> instead.
388            
389             =head1 AUTHOR
390            
391             Marc Mims <marc@questright.com>
392            
393             =head1 LICENSE
394            
395             Copyright (c) 2016 Marc Mims
396            
397             The Twitter API itself, and the description text used in this module is:
398            
399             Copyright (c) 2009 Twitter
400            
401             This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
402            
403             =head1 DISCLAIMER OF WARRANTY
404            
405             BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
406             FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
407             OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
408             PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
409             EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
410             WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
411             ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH
412             YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
413             NECESSARY SERVICING, REPAIR, OR CORRECTION.
414            
415             IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
416             WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
417             REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENSE, BE
418             LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL,
419             OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE
420             THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
421             RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
422             FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
423             SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
424             SUCH DAMAGES.
425