File Coverage

blib/lib/AnyEvent/Twitter.pm
Criterion Covered Total %
statement 61 138 44.2
branch 2 32 6.2
condition 6 18 33.3
subroutine 19 30 63.3
pod 7 7 100.0
total 95 225 42.2


line stmt bran cond sub pod time code
1             package AnyEvent::Twitter;
2 3     3   1846 use strict;
  3         7  
  3         108  
3 3     3   13 use warnings;
  3         5  
  3         85  
4 3     3   3465 use utf8;
  3         30  
  3         16  
5 3     3   157 use 5.008;
  3         9  
  3         182  
6             our $VERSION = '0.64';
7              
8 3     3   18 use Carp;
  3         4  
  3         270  
9 3     3   4365 use JSON;
  3         52276  
  3         17  
10 3     3   3641 use URI;
  3         15912  
  3         90  
11 3     3   24 use URI::Escape;
  3         5  
  3         196  
12 3     3   4139 use Digest::SHA;
  3         14228  
  3         166  
13 3     3   6253 use Time::Piece;
  3         75073  
  3         1505  
14 3     3   4374 use AnyEvent::HTTP;
  3         163886  
  3         333  
15 3     3   4793 use HTTP::Request::Common 'POST';
  3         102613  
  3         445  
16 3     3   17585 use Data::Recursive::Encode;
  3         56358  
  3         118  
17              
18 3     3   5511 use Net::OAuth;
  3         2302  
  3         99  
19 3     3   4632 use Net::OAuth::ProtectedResourceRequest;
  3         79102  
  3         38  
20 3     3   4156 use Net::OAuth::RequestTokenRequest;
  3         600  
  3         55  
21 3     3   5771 use Net::OAuth::AccessTokenRequest;
  3         1867  
  3         28  
22              
23             $Net::OAuth::PROTOCOL_VERSION = Net::OAuth::PROTOCOL_VERSION_1_0A;
24              
25             our %PATH = (
26             site => 'https://twitter.com/',
27             request_token => 'https://api.twitter.com/oauth/request_token',
28             authorize => 'https://api.twitter.com/oauth/authorize',
29             access_token => 'https://api.twitter.com/oauth/access_token',
30             authenticate => 'https://api.twitter.com/oauth/authenticate',
31             );
32              
33             our %RESOURCE_URL_BASE = (
34             '1.0' => 'http://api.twitter.com/1/%s.json',
35             '1.1' => 'https://api.twitter.com/1.1/%s.json',
36             );
37              
38             sub new {
39 3     3 1 42 my ($class, %args) = @_;
40              
41 3   50     18 $args{api_version} ||= '1.1';
42 3   100     14 $args{access_token} ||= $args{token};
43 3   66     12 $args{access_token_secret} ||= $args{token_secret};
44              
45 3         9 my @required = qw(access_token access_token_secret consumer_key consumer_secret);
46 3         7 for my $item (@required) {
47 9 100       328 defined $args{$item} or Carp::croak "$item is required";
48             }
49              
50 2         19 return bless { %args }, $class;
51             }
52              
53             sub get {
54 0     0 1 0 my $cb = pop;
55 0         0 my ($self, $endpoint, $params) = @_;
56              
57 0 0       0 my $type = $endpoint =~ /^http.+\.json$/ ? 'url' : 'api';
58 0         0 $self->request($type => $endpoint, method => 'GET', params => $params, $cb);
59              
60 0         0 return $self;
61             }
62              
63             sub post {
64 0     0 1 0 my ($self, $endpoint, $params, $cb) = @_;
65              
66 0 0       0 my $type = $endpoint =~ /^http.+\.json$/ ? 'url' : 'api';
67 0         0 $self->request($type => $endpoint, method => 'POST', params => $params, $cb);
68              
69 0         0 return $self;
70             }
71              
72             sub request {
73 0     0 1 0 my $cb = pop;
74 0         0 my ($self, %opt) = @_;
75              
76 0 0 0     0 ($opt{api} || $opt{url})
77             or Carp::croak "'api' or 'url' option is required";
78              
79 0         0 my $url_base = $RESOURCE_URL_BASE{ $self->{api_version} };
80 0   0     0 my $url = $opt{url} || sprintf $url_base, $opt{api};
81              
82 0 0       0 ref $cb eq 'CODE'
83             or Carp::croak "callback coderef is required";
84              
85 0   0     0 my $params = $opt{params} || {};
86 0         0 my $is_multipart = ref $params eq 'ARRAY';
87              
88 0         0 my $method = uc $opt{method};
89 0 0       0 $method =~ /^(?:GET|POST)$/
90             or Carp::croak "'method' option should be GET or POST";
91              
92 0 0       0 my $req = $self->_make_oauth_request(
93             class => 'Net::OAuth::ProtectedResourceRequest',
94             request_url => $url,
95             request_method => $method,
96             extra_params => ($is_multipart ? {} : $params),
97             consumer_key => $self->{consumer_key},
98             consumer_secret => $self->{consumer_secret},
99             token => $self->{access_token},
100             token_secret => $self->{access_token_secret},
101             );
102              
103 0         0 my $req_params = {};
104              
105 0 0       0 if ($method eq 'POST') {
106 0         0 $url = $req->normalized_request_url;
107              
108 0 0       0 if ($is_multipart) {
109             my $encoded_params = Data::Recursive::Encode::_apply(
110 0 0   0   0 sub { utf8::is_utf8($_[0]) ? Encode::encode_utf8($_[0]) : $_[0] },
111             {},
112 0         0 $params
113             );
114              
115 0         0 my $ireq = POST(
116             $url,
117             Content_Type => 'multipart/form-data',
118             Content => [ @$encoded_params ]
119             );
120              
121 0         0 $req_params->{body} = $ireq->content;
122 0         0 $req_params->{headers} = {
123             Authorization => $req->to_authorization_header,
124             'Content-Type' => join "; ", $ireq->content_type,
125             };
126              
127             } else {
128 0         0 $req_params->{body} = $req->to_post_body;
129 0         0 $req_params->{headers}{'Content-Type'} = 'application/x-www-form-urlencoded';
130             }
131              
132             } else {
133 0         0 $url = $req->to_url;
134             }
135              
136             AnyEvent::HTTP::http_request $method => $url, %$req_params, sub {
137 0     0   0 my ($body, $hdr) = @_;
138              
139 0         0 local $@;
140 0         0 my $json = eval { JSON::decode_json($body) };
  0         0  
141              
142 0 0       0 if ($hdr->{Status} =~ /^2/) {
143 0 0       0 $cb->($hdr, $json, $@ ? "parse error: $@" : $hdr->{Reason});
144             } else {
145 0         0 $cb->($hdr, undef, $hdr->{Reason}, $json);
146             }
147 0         0 };
148              
149 0         0 return $self;
150             }
151              
152             sub _make_oauth_request {
153 0     0   0 my $self = shift;
154 0         0 my %opt = @_;
155 0         0 my $class = delete $opt{class};
156              
157 0         0 local $Net::OAuth::SKIP_UTF8_DOUBLE_ENCODE_CHECK = 1;
158 0         0 my $req = $class->new(
159             version => '1.0',
160             timestamp => time,
161             nonce => Digest::SHA::sha1_base64(time . $$ . rand),
162             signature_method => 'HMAC-SHA1',
163             %opt,
164             );
165 0         0 $req->sign;
166              
167 0         0 return $req;
168             }
169              
170             sub get_request_token {
171 0     0 1 0 my ($class, %args) = @_;
172              
173 0         0 my @required = qw(consumer_key consumer_secret callback_url);
174 0         0 for my $item (@required) {
175 0 0       0 defined $args{$item} or Carp::croak "$item is required";
176             }
177              
178 0 0       0 ref $args{cb} eq 'CODE'
179             or Carp::croak "cb must be callback coderef";
180              
181 0   0     0 $args{auth} ||= 'authorize';
182              
183 0         0 my $req = __PACKAGE__->_make_oauth_request(
184             class => 'Net::OAuth::RequestTokenRequest',
185             request_method => 'GET',
186             request_url => $PATH{request_token},
187             consumer_key => $args{consumer_key},
188             consumer_secret => $args{consumer_secret},
189             callback => $args{callback_url},
190             );
191              
192             AnyEvent::HTTP::http_request GET => $req->to_url, sub {
193 0     0   0 my ($body, $header) = @_;
194 0         0 my %token = __PACKAGE__->_parse_response($body);
195 0         0 my $location = URI->new($PATH{ $args{auth} });
196 0         0 $location->query_form(%token);
197              
198 0         0 $args{cb}->($location->as_string, \%token, $body, $header);
199 0         0 };
200             }
201              
202             sub get_access_token {
203 0     0 1 0 my ($class, %args) = @_;
204              
205 0         0 my @required = qw(
206             consumer_key consumer_secret
207             oauth_token oauth_token_secret oauth_verifier
208             );
209              
210 0         0 for my $item (@required) {
211 0 0       0 defined $args{$item} or Carp::croak "$item is required";
212             }
213              
214 0 0       0 ref $args{cb} eq 'CODE'
215             or Carp::croak "cb must be callback coderef";
216              
217 0         0 my $req = __PACKAGE__->_make_oauth_request(
218             class => 'Net::OAuth::AccessTokenRequest',
219             request_method => 'GET',
220             request_url => $PATH{access_token},
221             consumer_key => $args{consumer_key},
222             consumer_secret => $args{consumer_secret},
223             token => $args{oauth_token},
224             token_secret => $args{oauth_token_secret},
225             verifier => $args{oauth_verifier},
226             );
227              
228             AnyEvent::HTTP::http_request GET => $req->to_url, sub {
229 0     0   0 my ($body, $header) = @_;
230 0         0 my %response = __PACKAGE__->_parse_response($body);
231 0         0 $args{cb}->(\%response, $body, $header);
232 0         0 };
233             }
234              
235             sub _parse_response {
236 0     0   0 my ($class, $body) = @_;
237              
238 0         0 my %query;
239 0         0 for my $pair (split /&/, $body) {
240 0         0 my ($key, $value) = split /=/, $pair;
241 0         0 $query{$key} = URI::Escape::uri_unescape($value);
242             }
243              
244 0         0 return %query;
245             }
246              
247             sub parse_timestamp { # Twitter uses weird created_at format: "Thu Mar 01 17:38:56 +0000 2012"
248 2     2 1 115 my ($class, $created_at) = @_;
249 2         15 localtime( Time::Piece->strptime($created_at, '%a %b %d %H:%M:%S %z %Y' )->epoch )
250             }
251              
252             1;
253             __END__