File Coverage

blib/lib/Net/Twitter/Core.pm
Criterion Covered Total %
statement 146 151 96.6
branch 38 46 82.6
condition 25 38 65.7
subroutine 38 40 95.0
pod 0 2 0.0
total 247 277 89.1


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