File Coverage

blib/lib/Net/Twitter/Core.pm
Criterion Covered Total %
statement 144 150 96.0
branch 41 50 82.0
condition 27 41 65.8
subroutine 38 40 95.0
pod 0 2 0.0
total 250 283 88.3


line stmt bran cond sub pod time code
1             package Net::Twitter::Core;
2             $Net::Twitter::Core::VERSION = '4.01043';
3             # ABSTRACT: A perl interface to the Twitter API
4              
5 41     41   2317 use 5.008001;
  40         114  
6 40     33   244 use Moose;
  33         56  
  33         1194  
7 33     33   180787 use Carp::Clan qw/^(?:Net::Twitter|Moose|Class::MOP)/;
  33         2535  
  33         164  
8 33     33   5040 use JSON::MaybeXS;
  33         62  
  33         4390  
9 33     33   9040 use URI::Escape;
  33         31059  
  33         1573  
10 33     33   12819 use HTTP::Request::Common;
  33         418765  
  33         1834  
11 33     33   12509 use Net::Twitter::Error;
  33         258  
  33         1003  
12 33     33   220 use Scalar::Util qw/blessed reftype/;
  33         59  
  33         1606  
13 33     33   163 use List::Util qw/first/;
  33         56  
  33         1384  
14 33     33   13437 use HTML::Entities ();
  33         150153  
  33         1032  
15 33     33   13944 use Encode qw/encode_utf8/;
  33         258198  
  33         1740  
16 33     33   21764 use DateTime;
  33         12217526  
  33         1271  
17 33     33   12790 use Data::Visitor::Callback;
  33         1678759  
  33         963  
18 33     33   213 use Try::Tiny;
  33         56  
  33         1577  
19              
20 33     33   167 use namespace::autoclean;
  33         56  
  33         199  
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   2557 sub _legacy_synthetic_args { qw/authenticate since/ }
56              
57             sub _remap_legacy_synthetic_args {
58 668     668   1163     my ( $self, $args ) = @_;
59              
60 668         1270     $args->{"-$_"} = delete $args->{$_} for grep exists $args->{$_}, $self->_legacy_synthetic_args;
61             }
62              
63             sub _natural_args {
64 667     667   985     my ( $self, $args ) = @_;
65              
66 667         1899     map { $_ => $args->{$_} } grep !/^-/, keys %$args;
  314         927  
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 6172     my $self = shift;
112              
113 113 100       3003     if ( $self->has_netrc ) {
114 2         9         require Net::Netrc;
115              
116             # accepts '1' for backwards compatibility
117 2 50       42         my $host = $self->netrc eq '1' ? $self->netrc_machine : $self->netrc;
118 2   33     10         my $nrc = Net::Netrc->lookup($host)
119                         || croak "No .netrc entry for $host";
120              
121 2         386         my ($user, $pass) = $nrc->lpa;
122 2         56         $self->username($user);
123 2         43         $self->password($pass);
124                 }
125              
126 113 100       3070     $self->credentials($self->username, $self->password) if $self->has_username;
127             }
128              
129             sub _build_ua {
130 33     33   73     my $self = shift;
131              
132 33     25   945     eval "use " . $self->useragent_class;
  25         4608  
  25         124981  
  25         416  
133 33 100       148     croak $@ if $@;
134              
135 32         897     my $ua = $self->useragent_class->new(%{$self->useragent_args});
  32         833  
136 32         63975     $ua->agent($self->useragent);
137 32         2756     $ua->default_header('X-Twitter-Client' => $self->clientname);
138 32         2767     $ua->default_header('X-Twitter-Client-Version' => $self->clientver);
139 32         2527     $ua->default_header('X-Twitter-Client-URL' => $self->clienturl);
140 32         1717     $ua->env_proxy;
141              
142 32         82707     return $ua;
143             }
144              
145             sub credentials {
146 45     45 0 907     my ($self, $username, $password) = @_;
147              
148 45         1013     $self->username($username);
149 45         999     $self->password($password);
150              
151 45         545     return $self; # make it chainable
152             }
153              
154             sub _encode_args {
155 667     667   909     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       1124     return { map { utf8::upgrade($_) unless ref($_); $_ } %$args };
  628         1574  
  628         1210  
162             }
163              
164             sub _json_request {
165 667     667   2056     my ($self, $http_method, $uri, $args, $authenticate, $dt_parser, $content_type ) = @_;
166              
167 667         1485     my $msg = $self->_prepare_request($http_method, $uri, $args, $authenticate, $content_type);
168 667         1351     my $res = $self->_send_request($msg);
169              
170 666         517006     return $self->_parse_result($res, $args, $dt_parser);
171             }
172              
173             sub _prepare_request {
174 667     667   1250     my ($self, $http_method, $uri, $args, $authenticate, $content_type ) = @_;
175              
176 667         783     my $msg;
177              
178 667         1239     my %natural_args = $self->_natural_args($args);
179              
180 667         1524     $self->_encode_args(\%natural_args);
181 667 50       2944     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         930         $uri->query($self->_query_string_for(\%natural_args));
189 421         8997         $msg = HTTP::Request->new($http_method, $uri);
190                 }
191                 elsif ( $http_method eq 'POST' ) {
192 246 50 33     559         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 175     175   412             $msg = (first { ref } values %natural_args)
198                             ? POST($uri,
199                                    Content_Type => 'form-data',
200                                    Content => [
201 246 100       1309                            map { ref $_ ? $_ : encode_utf8 $_ } %natural_args,
  6 100       47  
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       75561     $self->_add_authorization_header($msg, \%natural_args) if $authenticate;
213              
214 667         49218     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   1001     my ( $self, $args ) = @_;
221              
222 665         809     my @pairs;
223 665         1744     while ( my ($k, $v) = each %$args ) {
224 311         7570         push @pairs, join '=', map URI::Escape::uri_escape_utf8($_,'^\w.~-'), $k, $v;
225                 }
226              
227 665         16678     return join '&', @pairs;
228             }
229              
230             # Basic Auth, overridden by Role::OAuth, if included
231             sub _add_authorization_header {
232 641     641   1038     my ( $self, $msg ) = @_;
233              
234 641 100 66     18589     $msg->headers->authorization_basic($self->username, $self->password)
235                     if $self->has_username && $self->has_password;
236             }
237              
238 672     672   15057 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   836 sub _inflate_objects { return $_[2] }
259              
260             sub _parse_result {
261 666     666   1194     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         1330     my $content = $res->content;
266 666         6941     $content =~ s/^"(true|false)"$/$1/;
267              
268 666 100   664   3486     my $obj = length $content ? try { $self->from_json($content) } : {};
  664         15502  
269 666 50 66     29331     $self->_decode_html_entities($obj) if $obj && $self->decode_html_entities;
270              
271             # filter before inflating objects
272 666 100 66     1820     if ( (my $since = delete $args->{-since}) && defined $obj ) {
273 5         21         $self->_filter_since($datetime_parser, $obj, $since);
274                 }
275              
276             # inflate the twitter object(s) if possible
277 665         7210     $self->_inflate_objects($datetime_parser, $obj);
278              
279             # Twitter sometimes returns an error with status code 200
280 665 100 100     13632     if ( ref $obj && reftype $obj eq 'HASH' && (exists $obj->{error} || exists $obj->{errors}) ) {
      100        
      100        
281 5         141         die Net::Twitter::Error->new(twitter_error => $obj, http_response => $res);
282                 }
283              
284 660 100 100     1578     return $obj if $res->is_success && defined $obj;
285              
286 5 100       174     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   8     my ($self, $since, $parser) = @_;
301              
302 5 100 66     20     return $since if blessed($since) && $since->isa('DateTime');
303              
304 4 100       16     if ( my ($y, $m, $d) = $since =~ /^(\d{4})-(\d{2})-(\d{2})$/ ) {
305 1         5         return DateTime->new(month => $m, day => $d, year => $y);
306                 }
307              
308                 return eval { DateTime->from_epoch(epoch => $since) }
309 3   66     4         || 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         14     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   520             my ($visitor, $data) = @_;
324              
325 3 50       10             return unless $self->_contains_statuses($data);
326              
327             # truncate $data when we reach an item as old or older than $since_dt
328 3         7             my $i = 0;
329 3         6             while ( $i < @$data ) {
330 6 100       19                 last if $datetime_parser->parse_datetime($data->[$i]{created_at}) <= $since_dt;
331 3         2087                 ++$i;
332                         }
333 3         1963             $#{$data} = $i - 1;
  3         15  
334                     }
335 4         1255     );
336              
337 4         560     $visitor->visit($obj);
338             }
339              
340             # check an arrayref to see if it contains statuses
341             sub _contains_statuses {
342 3     3   7     my ($self, $arrayref) = @_;
343              
344 3   50     8     my $e = $arrayref->[0] || return;
345 3 50 33     23     return unless ref $e && reftype $e eq 'HASH';
346 3   33     17     return exists $e->{created_at} && exists $e->{text} && exists $e->{id};
347             }
348              
349             sub _user_or_undef {
350 4     4   13     my ( $self, $orig, $type, @rest ) = @_;
351              
352                 return try {
353 4     4   143         $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         25     };
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.01043
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