File Coverage

blib/lib/Mojo/WebService/Twitter.pm
Criterion Covered Total %
statement 222 368 60.3
branch 56 168 33.3
condition 19 77 24.6
subroutine 45 87 51.7
pod 21 21 100.0
total 363 721 50.3


line stmt bran cond sub pod time code
1             package Mojo::WebService::Twitter;
2 2     2   1327455 use Mojo::Base -base;
  2         21  
  2         18  
3              
4 2     2   448 use Carp 'croak';
  2         7  
  2         95  
5 2     2   13 use Scalar::Util 'blessed';
  2         6  
  2         117  
6 2     2   16 use Mojo::Collection;
  2         4  
  2         88  
7 2     2   13 use Mojo::UserAgent;
  2         5  
  2         25  
8 2     2   71 use Mojo::Util qw(b64_encode encode url_escape);
  2         5  
  2         165  
9 2     2   1014 use Mojo::WebService::Twitter::Error 'twitter_tx_error';
  2         6  
  2         110  
10 2     2   942 use Mojo::WebService::Twitter::Tweet;
  2         6  
  2         11  
11 2     2   72 use Mojo::WebService::Twitter::User;
  2         5  
  2         6  
12 2     2   48 use Mojo::WebService::Twitter::Util;
  2         4  
  2         63  
13 2     2   1475 use WWW::OAuth;
  2         33696  
  2         14571  
14              
15             our $VERSION = '1.003';
16              
17             has ['api_key','api_secret'];
18             has 'ua' => sub { Mojo::UserAgent->new };
19              
20             sub authentication {
21 12     12 1 1583 my $self = shift;
22 12 100 66     332 return $self->{authentication} // croak 'No authentication set' unless @_;
23 2         7 my $auth = shift;
24 2 50       18 if (ref $auth eq 'CODE') {
    50          
    100          
    50          
25 0         0 $self->{authentication} = $auth;
26             } elsif (ref $auth eq 'HASH') {
27 0 0 0     0 if (defined $auth->{access_token}) {
    0          
28 0         0 $self->{authentication} = $self->_oauth2($auth->{access_token});
29             } elsif (defined $auth->{oauth_token} and defined $auth->{oauth_token_secret}) {
30 0         0 $self->{authentication} = $self->_oauth($auth->{oauth_token}, $auth->{oauth_token_secret});
31             } else {
32 0         0 croak 'Unrecognized authentication hashref (no oauth_token or access_token)';
33             }
34             } elsif ($auth eq 'oauth') {
35 1         4 $self->{authentication} = $self->_oauth(@_);
36             } elsif ($auth eq 'oauth2') {
37 1         8 $self->{authentication} = $self->_oauth2(@_);
38             } else {
39 0         0 croak "Unknown authentication $auth";
40             }
41 2         7 return $self;
42             }
43              
44             sub request_oauth {
45 1 50   1 1 127 my $cb = ref $_[-1] eq 'CODE' ? pop : undef;
46 1         2 my $self = shift;
47 1         5 my $tx = $self->_build_request_oauth(@_);
48 1 50       15 if ($cb) {
49             $self->ua->start($tx, sub {
50 0     0   0 my ($ua, $tx) = @_;
51 0 0       0 return $self->$cb(twitter_tx_error($tx)) if $tx->error;
52 0   0     0 my $res = $self->_from_request_oauth($tx) // return $self->$cb('OAuth callback was not confirmed');
53 0         0 $self->$cb(undef, $res);
54 0         0 });
55             } else {
56 1         5 $tx = $self->ua->start($tx);
57 1 50       18712 die twitter_tx_error($tx) if $tx->error;
58 1   50     21 return $self->_from_request_oauth($tx) // die "OAuth callback was not confirmed\n";
59             }
60             }
61              
62             sub request_oauth_p {
63 0     0 1 0 my $self = shift;
64 0         0 my $tx = $self->_build_request_oauth(@_);
65             return $self->ua->start_p($tx)->then(sub {
66 0     0   0 my ($tx) = @_;
67 0 0       0 die twitter_tx_error($tx) if $tx->error;
68 0   0     0 return $self->_from_request_oauth($tx) // die "OAuth callback was not confirmed\n";
69 0     0   0 }, sub { die Mojo::WebService::Twitter::Error->new(connection_error => $_[0]) });
  0         0  
70             }
71              
72             sub _build_request_oauth {
73 1     1   3 my ($self, $url) = @_;
74 1   50     9 $url //= 'oob';
75 1         21 my $tx = $self->ua->build_tx(POST => _oauth_url('request_token'));
76 1         564 $self->_oauth->($tx->req, { oauth_callback => $url });
77 1         10170 return $tx;
78             }
79              
80             sub _from_request_oauth {
81 1     1   3 my ($self, $tx) = @_;
82 1         5 my $params = Mojo::Parameters->new($tx->res->text)->to_hash;
83             return undef unless $params->{oauth_callback_confirmed} eq 'true'
84 1 50 33     335 and defined $params->{oauth_token} and defined $params->{oauth_token_secret};
      33        
85 1         6 $self->{request_token_secrets}{$params->{oauth_token}} = $params->{oauth_token_secret};
86 1         5 return $params;
87             }
88              
89             sub verify_oauth {
90 1 50   1 1 1594 my $cb = ref $_[-1] eq 'CODE' ? pop : undef;
91 1         3 my $self = shift;
92 1         4 my $tx = $self->_build_verify_oauth(@_);
93 1 50       15 if ($cb) {
94             $self->ua->start($tx, sub {
95 0     0   0 my ($ua, $tx) = @_;
96 0 0       0 return $self->$cb(twitter_tx_error($tx)) if $tx->error;
97 0   0     0 my $res = $self->_from_verify_oauth($tx) // return $self->$cb('No OAuth token returned');
98 0         0 $self->$cb(undef, $res);
99 0         0 });
100             } else {
101 1         5 $tx = $self->ua->start($tx);
102 1 50       8237 die twitter_tx_error($tx) if $tx->error;
103 1   50     22 return $self->_from_verify_oauth($tx) // die "No OAuth token returned\n";
104             }
105             }
106              
107             sub verify_oauth_p {
108 0     0 1 0 my $self = shift;
109 0         0 my $tx = $self->_build_verify_oauth(@_);
110             return $self->ua->start_p($tx)->then(sub {
111 0     0   0 my ($tx) = @_;
112 0 0       0 die twitter_tx_error($tx) if $tx->error;
113 0   0     0 return $self->_from_verify_oauth($tx) // die "No OAuth token returned\n";
114 0     0   0 }, sub { die Mojo::WebService::Twitter::Error->new(connection_error => $_[0]) });
  0         0  
115             }
116              
117             sub _build_verify_oauth {
118 1     1   4 my ($self, $verifier, $request_token, $request_token_secret) = @_;
119 1   33     9 $request_token_secret //= delete $self->{request_token_secrets}{$request_token} // croak "Unknown request token";
      33        
120 1         4 my $tx = $self->ua->build_tx(POST => _oauth_url('access_token'));
121 1         316 $self->_oauth($request_token, $request_token_secret)->($tx->req, { oauth_verifier => $verifier });
122 1         1387 return $tx;
123             }
124              
125             sub _from_verify_oauth {
126 1     1   3 my ($self, $tx) = @_;
127 1         4 my $params = Mojo::Parameters->new($tx->res->text)->to_hash;
128 1 50 33     235 return undef unless defined $params->{'oauth_token'} and defined $params->{'oauth_token_secret'};
129 1         5 return $params;
130             }
131              
132             sub request_oauth2 {
133 1 50   1 1 1795 my $cb = ref $_[-1] eq 'CODE' ? pop : undef;
134 1         3 my $self = shift;
135 1         6 my $tx = $self->_build_request_oauth2(@_);
136 1 50       5 if ($cb) {
137             $self->ua->start($tx, sub {
138 0     0   0 my ($ua, $tx) = @_;
139 0 0       0 return $self->$cb(twitter_tx_error($tx)) if $tx->error;
140 0   0     0 my $res = $self->_from_request_oauth2($tx) // return $self->$cb('No bearer token returned');
141 0         0 $self->$cb(undef, $res);
142 0         0 });
143             } else {
144 1         4 $tx = $self->ua->start($tx);
145 1 50       21961 die twitter_tx_error($tx) if $tx->error;
146 1   50     27 return $self->_from_request_oauth2($tx) // die "No bearer token returned\n";
147             }
148             }
149              
150             sub request_oauth2_p {
151 0     0 1 0 my $self = shift;
152 0         0 my $tx = $self->_build_request_oauth2(@_);
153             return $self->ua->start_p($tx)->then(sub {
154 0     0   0 my ($tx) = @_;
155 0 0       0 die twitter_tx_error($tx) if $tx->error;
156 0   0     0 return $self->_from_request_oauth2($tx) // die "No bearer token returned\n";
157 0     0   0 }, sub { die Mojo::WebService::Twitter::Error->new(connection_error => $_[0]) });
  0         0  
158             }
159              
160             sub _build_request_oauth2 {
161 1     1   2 my ($self) = @_;
162 1         6 my $tx = $self->ua->build_tx(POST => _oauth2_url('token'), form => { grant_type => 'client_credentials' });
163 1         980 $self->_oauth2_request->($tx->req);
164 1         20 return $tx;
165             }
166              
167             sub _from_request_oauth2 {
168 1     1   3 my ($self, $tx) = @_;
169 1   50     4 my $params = $tx->res->json // {};
170 1 50       254 return undef unless defined $params->{access_token};
171 1         7 return $params;
172             }
173              
174             sub get_tweet {
175 4 50   4 1 14839 my $cb = ref $_[-1] eq 'CODE' ? pop : undef;
176 4         11 my $self = shift;
177 4         19 my $tx = $self->_build_get_tweet(@_);
178 3 50       14 if ($cb) {
179             $self->ua->start($tx, sub {
180 0     0   0 my ($ua, $tx) = @_;
181 0 0       0 return $self->$cb(twitter_tx_error($tx)) if $tx->error;
182 0         0 $self->$cb(undef, _tweet_object($tx->res->json));
183 0         0 });
184             } else {
185 3         12 $tx = $self->ua->start($tx);
186 3 50       28967 die twitter_tx_error($tx) if $tx->error;
187 3         62 return _tweet_object($tx->res->json);
188             }
189             }
190              
191             sub get_tweet_p {
192 0     0 1 0 my $self = shift;
193 0         0 my $tx = $self->_build_get_tweet(@_);
194             return $self->ua->start_p($tx)->then(sub {
195 0     0   0 my ($tx) = @_;
196 0 0       0 die twitter_tx_error($tx) if $tx->error;
197 0         0 return _tweet_object($tx->res->json);
198 0     0   0 }, sub { die Mojo::WebService::Twitter::Error->new(connection_error => $_[0]) });
  0         0  
199             }
200              
201             sub _build_get_tweet {
202 4     4   12 my ($self, $id) = @_;
203 4 50       14 croak 'Tweet ID is required for get_tweet' unless defined $id;
204 4 50       34 croak 'Invalid tweet ID to retrieve' unless $id =~ m/\A[0-9]+\z/;
205 4         25 my $tx = $self->ua->build_tx(GET => _api_url('statuses/show.json')
206             ->query(_www_form_urlencode(id => $id, tweet_mode => 'extended')));
207 4         903 $self->authentication->($tx->req);
208 3         83 return $tx;
209             }
210              
211             sub get_user {
212 2 50   2 1 3384 my $cb = ref $_[-1] eq 'CODE' ? pop : undef;
213 2         7 my $self = shift;
214 2         8 my $tx = $self->_build_get_user(@_);
215 2 50       7 if ($cb) {
216             $self->ua->start($tx, sub {
217 0     0   0 my ($ua, $tx) = @_;
218 0 0       0 return $self->$cb(twitter_tx_error($tx)) if $tx->error;
219 0         0 $self->$cb(undef, _user_object($tx->res->json));
220 0         0 });
221             } else {
222 2         10 $tx = $self->ua->start($tx);
223 2 50       18327 die twitter_tx_error($tx) if $tx->error;
224 2         40 return _user_object($tx->res->json);
225             }
226             }
227              
228             sub get_user_p {
229 0     0 1 0 my $self = shift;
230 0         0 my $tx = $self->_build_get_user(@_);
231             return $self->ua->start_p($tx)->then(sub {
232 0     0   0 my ($tx) = @_;
233 0 0       0 die twitter_tx_error($tx) if $tx->error;
234 0         0 return _user_object($tx->res->json);
235 0     0   0 }, sub { die Mojo::WebService::Twitter::Error->new(connection_error => $_[0]) });
  0         0  
236             }
237              
238             sub _build_get_user {
239 2     2   8 my ($self, %params) = @_;
240 2         4 my %query;
241 2 100       11 $query{user_id} = $params{user_id} if defined $params{user_id};
242 2 100       9 $query{screen_name} = $params{screen_name} if defined $params{screen_name};
243 2 50       5 croak 'user_id or screen_name is required for get_user' unless %query;
244 2         5 $query{tweet_mode} = 'extended';
245 2         17 my $tx = $self->ua->build_tx(GET => _api_url('users/show.json')->query(_www_form_urlencode(%query)));
246 2         311 $self->authentication->($tx->req);
247 2         33 return $tx;
248             }
249              
250             sub get_user_timeline {
251 1 50   1 1 5604 my $cb = ref $_[-1] eq 'CODE' ? pop : undef;
252 1         3 my $self = shift;
253 1         7 my $tx = $self->_build_get_user_timeline(@_);
254 1 50       5 if ($cb) {
255             $self->ua->start($tx, sub {
256 0     0   0 my ($ua, $tx) = @_;
257 0 0       0 return $self->$cb(twitter_tx_error($tx)) if $tx->error;
258 0   0     0 $self->$cb(undef, Mojo::Collection->new(map { _tweet_object($_) } @{$tx->res->json // []}));
  0         0  
  0         0  
259 0         0 });
260             } else {
261 1         4 $tx = $self->ua->start($tx);
262 1 50       11334 die twitter_tx_error($tx) if $tx->error;
263 1   50     20 return Mojo::Collection->new(map { _tweet_object($_) } @{$tx->res->json // []});
  20         68805  
  1         3  
264             }
265             }
266              
267             sub get_user_timeline_p {
268 0     0 1 0 my $self = shift;
269 0         0 my $tx = $self->_build_get_user_timeline(@_);
270             return $self->ua->start_p($tx)->then(sub {
271 0     0   0 my ($tx) = @_;
272 0 0       0 die twitter_tx_error($tx) if $tx->error;
273 0   0     0 return Mojo::Collection->new(map { _tweet_object($_) } @{$tx->res->json // []});
  0         0  
  0         0  
274 0     0   0 }, sub { die Mojo::WebService::Twitter::Error->new(connection_error => $_[0]) });
  0         0  
275             }
276              
277             sub _build_get_user_timeline {
278 1     1   5 my ($self, %params) = @_;
279 1         3 my %query;
280 1 50       7 $query{user_id} = $params{user_id} if defined $params{user_id};
281 1 50       5 $query{screen_name} = $params{screen_name} if defined $params{screen_name};
282 1 50       3 croak 'user_id or screen_name is required for get_user_timeline' unless %query;
283 1         3 $query{$_} = $params{$_} for grep { defined $params{$_} } qw(count since_id max_id);
  3         11  
284 1 50       5 $query{exclude_replies} = $params{exclude_replies} ? 'true' : 'false';
285 1 50       7 $query{include_rts} = $params{exclude_rts} ? 'false' : 'true';
286 1         3 $query{trim_user} = 'true';
287 1         3 $query{tweet_mode} = 'extended';
288 1         7 my $tx = $self->ua->build_tx(GET => _api_url('statuses/user_timeline.json')->query(_www_form_urlencode(%query)));
289 1         200 $self->authentication->($tx->req);
290 1         19 return $tx;
291             }
292              
293             sub post_tweet {
294 1 50   1 1 871 my $cb = ref $_[-1] eq 'CODE' ? pop : undef;
295 1         3 my $self = shift;
296 1         5 my $tx = $self->_build_post_tweet(@_);
297 1 50       4 if ($cb) {
298             $self->ua->start($tx, sub {
299 0     0   0 my ($ua, $tx) = @_;
300 0 0       0 return $self->$cb(twitter_tx_error($tx)) if $tx->error;
301 0         0 $self->$cb(undef, _tweet_object($tx->res->json));
302 0         0 });
303             } else {
304 1         4 $tx = $self->ua->start($tx);
305 1 50       8951 die twitter_tx_error($tx) if $tx->error;
306 1         21 return _tweet_object($tx->res->json);
307             }
308             }
309              
310             sub post_tweet_p {
311 0     0 1 0 my $self = shift;
312 0         0 my $tx = $self->_build_post_tweet(@_);
313             return $self->ua->start_p($tx)->then(sub {
314 0     0   0 my ($tx) = @_;
315 0 0       0 die twitter_tx_error($tx) if $tx->error;
316 0         0 return _tweet_object($tx->res->json);
317 0     0   0 }, sub { die Mojo::WebService::Twitter::Error->new(connection_error => $_[0]) });
  0         0  
318             }
319              
320             sub _build_post_tweet {
321 1     1   3 my ($self, $status, %params) = @_;
322 1 50       4 croak 'Status text is required to post a tweet' unless defined $status;
323 1         3 my %form;
324 1         3 $form{status} = $status;
325 1         3 $form{$_} = $params{$_} for grep { defined $params{$_} }
  4         12  
326             qw(in_reply_to_status_id lat long place_id);
327 1 0       3 $form{$_} = $params{$_} ? 'true' : 'false' for grep { defined $params{$_} }
  1         3  
328             qw(display_coordinates);
329 1         4 $form{tweet_mode} = 'extended';
330 1         4 my $tx = $self->ua->build_tx(POST => _api_url('statuses/update.json'),
331             {'Content-Type' => 'application/x-www-form-urlencoded'}, _www_form_urlencode(%form));
332 1         188 $self->authentication->($tx->req);
333 1         1706 return $tx;
334             }
335              
336             sub retweet {
337 0 0   0 1 0 my $cb = ref $_[-1] eq 'CODE' ? pop : undef;
338 0         0 my $self = shift;
339 0         0 my $tx = $self->_build_retweet(@_);
340 0 0       0 if ($cb) {
341             $self->ua->start($tx, sub {
342 0     0   0 my ($ua, $tx) = @_;
343 0 0       0 return $self->$cb(twitter_tx_error($tx)) if $tx->error;
344 0         0 $self->$cb(undef, _tweet_object($tx->res->json));
345 0         0 });
346             } else {
347 0         0 $tx = $self->ua->start($tx);
348 0 0       0 die twitter_tx_error($tx) if $tx->error;
349 0         0 return _tweet_object($tx->res->json);
350             }
351             }
352              
353             sub retweet_p {
354 0     0 1 0 my $self = shift;
355 0         0 my $tx = $self->_build_retweet(@_);
356             return $self->ua->start_p($tx)->then(sub {
357 0     0   0 my ($tx) = @_;
358 0 0       0 die twitter_tx_error($tx) if $tx->error;
359 0         0 return _tweet_object($tx->res->json);
360 0     0   0 }, sub { die Mojo::WebService::Twitter::Error->new(connection_error => $_[0]) });
  0         0  
361             }
362              
363             sub _build_retweet {
364 0     0   0 my ($self, $id) = @_;
365 0 0       0 croak 'Tweet ID is required for retweet' unless defined $id;
366 0 0 0     0 $id = $id->id if blessed $id and $id->isa('Mojo::WebService::Twitter::Tweet');
367 0 0       0 croak 'Invalid tweet ID to retweet' unless $id =~ m/\A[0-9]+\z/;
368 0         0 my $tx = $self->ua->build_tx(POST => _api_url("statuses/retweet/$id.json")->query(tweet_mode => 'extended'));
369 0         0 $self->authentication->($tx->req);
370 0         0 return $tx;
371             }
372              
373             sub search_tweets {
374 1 50   1 1 1772 my $cb = ref $_[-1] eq 'CODE' ? pop : undef;
375 1         3 my $self = shift;
376 1         7 my $tx = $self->_build_search_tweets(@_);
377 1 50       6 if ($cb) {
378             $self->ua->start($tx, sub {
379 0     0   0 my ($ua, $tx) = @_;
380 0 0       0 return $self->$cb(twitter_tx_error($tx)) if $tx->error;
381 0   0     0 $self->$cb(undef, Mojo::Collection->new(map { _tweet_object($_) } @{$tx->res->json->{statuses} // []}));
  0         0  
  0         0  
382 0         0 });
383             } else {
384 1         5 $tx = $self->ua->start($tx);
385 1 50       11424 die twitter_tx_error($tx) if $tx->error;
386 1   50     25 return Mojo::Collection->new(map { _tweet_object($_) } @{$tx->res->json->{statuses} // []});
  15         91095  
  1         3  
387             }
388             }
389              
390             sub search_tweets_p {
391 0     0 1 0 my $self = shift;
392 0         0 my $tx = $self->_build_search_tweets(@_);
393             return $self->ua->start_p($tx)->then(sub {
394 0     0   0 my ($tx) = @_;
395 0 0       0 die twitter_tx_error($tx) if $tx->error;
396 0   0     0 return Mojo::Collection->new(map { _tweet_object($_) } @{$tx->res->json->{statuses} // []});
  0         0  
  0         0  
397 0     0   0 }, sub { die Mojo::WebService::Twitter::Error->new(connection_error => $_[0]) });
  0         0  
398             }
399              
400             sub _build_search_tweets {
401 1     1   4 my ($self, $q, %params) = @_;
402 1 50       5 croak 'Search query is required for search_tweets' unless defined $q;
403 1         2 my %query;
404 1         5 $query{q} = $q;
405 1         7 my $geocode = $params{geocode};
406 1 50       3 if (ref $geocode) {
407 0         0 my ($lat, $long, $rad);
408 0 0       0 ($lat, $long, $rad) = @$geocode if ref $geocode eq 'ARRAY';
409 0 0       0 ($lat, $long, $rad) = @{$geocode}{'latitude','longitude','radius'} if ref $geocode eq 'HASH';
  0         0  
410 0 0 0     0 $geocode = "$lat,$long,$rad" if defined $lat and defined $long and defined $rad;
      0        
411             }
412 1 50       4 $query{geocode} = $geocode if defined $geocode;
413 1         4 $query{$_} = $params{$_} for grep { defined $params{$_} } qw(lang result_type count until since_id max_id);
  6         16  
414 1         4 $query{tweet_mode} = 'extended';
415 1         7 my $tx = $self->ua->build_tx(GET => _api_url('search/tweets.json')->query(_www_form_urlencode(%query)));
416 1         235 $self->authentication->($tx->req);
417 1         21 return $tx;
418             }
419              
420             sub verify_credentials {
421 1 50   1 1 10 my $cb = ref $_[-1] eq 'CODE' ? pop : undef;
422 1         3 my $self = shift;
423 1         4 my $tx = $self->_build_verify_credentials(@_);
424 1 50       4 if ($cb) {
425             $self->ua->start($tx, sub {
426 0     0   0 my ($ua, $tx) = @_;
427 0 0       0 return $self->$cb(twitter_tx_error($tx)) if $tx->error;
428 0         0 $self->$cb(undef, Mojo::WebService::Twitter::User->new(twitter => $self)->from_source($tx->res->json));
429 0         0 });
430             } else {
431 1         3 $tx = $self->ua->start($tx);
432 1 50       9314 die twitter_tx_error($tx) if $tx->error;
433 1         30 return Mojo::WebService::Twitter::User->new(twitter => $self)->from_source($tx->res->json);
434             }
435             }
436              
437             sub verify_credentials_p {
438 0     0 1 0 my $self = shift;
439 0         0 my $tx = $self->_build_verify_credentials(@_);
440             return $self->ua->start_p($tx)->then(sub {
441 0     0   0 my ($tx) = @_;
442 0 0       0 die twitter_tx_error($tx) if $tx->error;
443 0         0 return Mojo::WebService::Twitter::User->new(twitter => $self)->from_source($tx->res->json);
444 0     0   0 }, sub { die Mojo::WebService::Twitter::Error->new(connection_error => $_[0]) });
  0         0  
445             }
446              
447             sub _build_verify_credentials {
448 1     1   33 my ($self) = @_;
449 1         8 my $tx = $self->ua->build_tx(GET => _api_url('account/verify_credentials.json')->query(tweet_mode => 'extended'));
450 1         448 $self->authentication->($tx->req);
451 1         1641 return $tx;
452             }
453              
454 10     10   109 sub _api_url { Mojo::WebService::Twitter::Util::_api_url(@_) }
455 2     2   20 sub _oauth_url { Mojo::WebService::Twitter::Util::_oauth_url(@_) }
456 1     1   11 sub _oauth2_url { Mojo::WebService::Twitter::Util::_oauth2_url(@_) }
457              
458             sub _oauth {
459 3     3   48 my $self = shift;
460 3         12 my ($api_key, $api_secret) = ($self->api_key, $self->api_secret);
461 3 50 33     36 croak 'Twitter API key and secret are required' unless defined $api_key and defined $api_secret;
462 3         8 my ($token, $token_secret) = @_;
463 3         22 my $oauth = WWW::OAuth->new(
464             client_id => $api_key,
465             client_secret => $api_secret,
466             token => $token,
467             token_secret => $token_secret,
468             );
469 3     4   234 return sub { $oauth->authenticate(@_) };
  4         15  
470             }
471              
472             sub _oauth2_request {
473 1     1   11 my $self = shift;
474 1         4 my ($api_key, $api_secret) = ($self->api_key, $self->api_secret);
475 1 50 33     15 croak 'Twitter API key and secret are required' unless defined $api_key and defined $api_secret;
476 1         4 my $token = b64_encode(url_escape($api_key) . ':' . url_escape($api_secret), '');
477 1     1   33 return sub { shift->headers->authorization("Basic $token") };
  1         5  
478             }
479              
480             sub _oauth2 {
481 1     1   3 my $self = shift;
482 1   33     4 my $token = shift // croak 'Access token is required for OAuth2 authentication';
483 1     7   7 return sub { shift->headers->authorization("Bearer $token") };
  7         27  
484             }
485              
486 39     39   24514 sub _tweet_object { Mojo::WebService::Twitter::Tweet->new->from_source(shift) }
487              
488 2     2   16756 sub _user_object { Mojo::WebService::Twitter::User->new->from_source(shift) }
489              
490             sub _www_form_urlencode {
491 9     9   2404 my @params = @_;
492 9         21 my @terms;
493 9         38 while (@params) {
494 21         256 my ($key, $values) = splice @params, 0, 2;
495 21 50       75 foreach my $value (ref $values eq 'ARRAY' ? @$values : $values) {
496 21   50     80 push @terms, join '=', map { url_escape encode 'UTF-8', $_ } ($key // ''), ($value // '');
  42   50     390  
497             }
498             }
499 9         192 return join '&', @terms;
500             }
501              
502             1;
503              
504             =head1 NAME
505              
506             Mojo::WebService::Twitter - Simple Twitter API client
507              
508             =head1 SYNOPSIS
509              
510             my $twitter = Mojo::WebService::Twitter->new(api_key => $api_key, api_secret => $api_secret);
511            
512             # Request and store access token
513             $twitter->authentication($twitter->request_oauth2);
514            
515             # Blocking API request
516             my $user = $twitter->get_user(screen_name => $name);
517             say $user->screen_name . ' was created on ' . $user->created_at->ymd;
518            
519             # Non-blocking API request
520             $twitter->get_tweet($tweet_id, sub {
521             my ($twitter, $err, $tweet) = @_;
522             print $err ? "Error: $err" : 'Tweet: ' . $tweet->text . "\n";
523             });
524             Mojo::IOLoop->start unless Mojo::IOLoop->is_running;
525            
526             # Non-blocking API request using promises
527             $twitter->get_tweet_p($tweet_id)->then(sub {
528             my ($tweet) = @_;
529             print 'Tweet: ' . $tweet->text . "\n";
530             })->catch(sub {
531             my ($err) = @_;
532             print "Error: $err";
533             })->wait;
534            
535             # Some requests require authentication on behalf of a user
536             $twitter->authentication(oauth => $token, $secret);
537             my $authorizing_user = $twitter->verify_credentials;
538            
539             my $new_tweet = $twitter->post_tweet('Something new and exciting!');
540              
541             =head1 DESCRIPTION
542              
543             L is a L based
544             L API client that can perform requests
545             synchronously or asynchronously. An API key and secret for a
546             L are required.
547              
548             API requests are authenticated by the L coderef, which can
549             either use an OAuth 2.0 access token to authenticate requests on behalf of the
550             application itself, or OAuth 1.0 credentials (access token and secret) to
551             authenticate requests on behalf of a specific user. The L
552             script can be used to obtain Twitter OAuth credentials for a user from the
553             command-line. A web application may wish to implement its own OAuth
554             authorization flow, passing a callback URL back to the application in
555             L, then calling L with the passed verifier
556             code to retrieve the credentials. See the
557             L for
558             more details.
559              
560             All methods which query the Twitter API can be called with an optional trailing
561             callback argument to run a non-blocking API query. Alternatively, the C<_p>
562             variant will run a non-blocking API query and return a L, which
563             can simplify complex sequences of non-blocking queries. On connection, HTTP, or
564             API error, blocking API queries will throw a L
565             exception. Non-blocking API queries will pass this exception object to the
566             callback or reject the promise, and otherwise pass the results to the callback
567             or resolve the promise.
568              
569             Note that this distribution implements only a subset of the Twitter API.
570             Additional features may be added as requested. See L for a more
571             fully-featured lightweight and modern twitter client library.
572              
573             =head1 ATTRIBUTES
574              
575             L implements the following attributes.
576              
577             =head2 api_key
578              
579             my $api_key = $twitter->api_key;
580             $twitter = $twitter->api_key($api_key);
581              
582             API key for your L.
583              
584             =head2 api_secret
585              
586             my $api_secret = $twitter->api_secret;
587             $twitter = $twitter->api_secret($api_secret);
588              
589             API secret for your L.
590              
591             =head2 ua
592              
593             my $ua = $webservice->ua;
594             $webservice = $webservice->ua(Mojo::UserAgent->new);
595              
596             HTTP user agent object to use for synchronous and asynchronous requests,
597             defaults to a L object.
598              
599             =head1 METHODS
600              
601             L inherits all methods from L, and
602             implements the following new ones.
603              
604             =head2 authentication
605              
606             my $code = $twitter->authentication;
607             $twitter = $twitter->authentication($code);
608             $twitter = $twitter->authentication({oauth_token => $access_token, oauth_token_secret => $access_token_secret});
609             $twitter = $twitter->authentication(oauth => $access_token, $access_token_secret);
610             $twitter = $twitter->authentication({access_token => $access_token});
611             $twitter = $twitter->authentication(oauth2 => $access_token);
612              
613             Get or set coderef used to authenticate API requests. Passing C with
614             optional token and secret, or a hashref containing C and
615             C, will set a coderef which uses a L to
616             authenticate requests. Passing C with required token or a hashref
617             containing C will set a coderef which authenticates using the
618             passed access token. The coderef will receive the L
619             object as the first parameter, and an optional hashref of C parameters.
620              
621             =head2 request_oauth
622              
623             =head2 request_oauth_p
624              
625             my $res = $twitter->request_oauth;
626             my $res = $twitter->request_oauth($callback_url);
627             $twitter->request_oauth(sub {
628             my ($twitter, $error, $res) = @_;
629             });
630             my $p = $twitter->request_oauth_p;
631              
632             Send an OAuth 1.0 authorization request and return a hashref containing
633             C and C (request token and secret). An
634             optional OAuth callback URL may be passed; by default, C is passed to use
635             PIN-based authorization. The user should be directed to the authorization URL
636             which can be retrieved by passing the request token to
637             L. After
638             authorization, the user will either be redirected to the callback URL with the
639             query parameter C, or receive a PIN to return to the
640             application. Either the verifier string or PIN should be passed to
641             L to retrieve an access token and secret.
642              
643             =head2 verify_oauth
644              
645             =head2 verify_oauth_p
646              
647             my $res = $twitter->verify_oauth($verifier, $request_token, $request_token_secret);
648             $twitter->verify_oauth($verifier, $request_token, $request_token_secret, sub {
649             my ($twitter, $error, $res) = @_;
650             });
651             my $p = $twitter->verify_oauth_p($verifier, $request_token, $request_token_secret);
652              
653             Verify an OAuth 1.0 authorization request with the verifier string or PIN from
654             the authorizing user, and the previously obtained request token and secret. The
655             secret is cached by L and may be omitted. Returns a hashref
656             containing C and C (access token and secret)
657             which may be passed directly to L to authenticate requests
658             on behalf of the user.
659              
660             =head2 request_oauth2
661              
662             =head2 request_oauth2_p
663              
664             my $res = $twitter->request_oauth2;
665             $twitter->request_oauth2(sub {
666             my ($twitter, $error, $res) = @_;
667             });
668             my $p = $twitter->request_oauth2_p;
669              
670             Request OAuth 2 credentials and return a hashref containing an C
671             that can be passed directly to L to authenticate requests on
672             behalf of the application itself.
673              
674             =head2 get_tweet
675              
676             =head2 get_tweet_p
677              
678             my $tweet = $twitter->get_tweet($tweet_id);
679             $twitter->get_tweet($tweet_id, sub {
680             my ($twitter, $err, $tweet) = @_;
681             });
682             $twitter->get_tweet_p($tweet_id);
683              
684             Retrieve a L by tweet ID.
685              
686             =head2 get_user
687              
688             =head2 get_user_p
689              
690             my $user = $twitter->get_user(user_id => $user_id);
691             my $user = $twitter->get_user(screen_name => $screen_name);
692             $twitter->get_user(screen_name => $screen_name, sub {
693             my ($twitter, $err, $user) = @_;
694             });
695             my $p = $twitter->get_user_p(user_id => $user_id);
696              
697             Retrieve a L by user ID or screen name.
698              
699             =head2 get_user_timeline
700              
701             =head2 get_user_timeline_p
702              
703             I
704              
705             my $tweets = $twitter->get_user_timeline(user_id => $user_id);
706             my $tweets = $twitter->get_user_timeline(screen_name => $screen_name);
707             my $tweets = $twitter->get_user_timeline(user_id => $user_id, %options);
708             $twitter->get_user_timeline(screen_name => $screen_name, %options, sub {
709             my ($twitter, $err, $tweets) = @_;
710             });
711             my $p = $twitter->get_user_timeline_p(screen_name => $screen_name, %options);
712              
713             Retrieve a L of L objects
714             for a user's timeline by user ID or screen name. Note that the embedded user
715             objects will only contain an C to avoid excess duplication of the same
716             user's information; use L to retrieve the user's information.
717              
718             Accepts the following options:
719              
720             =over
721              
722             =item count
723              
724             count => 5
725              
726             Limit of tweets to try and retrieve per page. Actual returned count may be
727             smaller due to filtering of content that is no longer available, RTs if the
728             L option is enabled, or replies if the L
729             option is enabled. Maximum C<200>, default C<20>.
730              
731             =item since_id
732              
733             since_id => '12345'
734              
735             Restricts results to those more recent than the given tweet ID. IDs should be
736             specified as a string to avoid issues with large integers. See
737             L
738             for more information on filtering results with C and C.
739              
740             =item max_id
741              
742             max_id => '54321'
743              
744             Restricts results to those older than (or equal to) the given tweet ID. IDs
745             should be specified as a string to avoid issues with large integers. See
746             L
747             for more information on filtering results with C and C.
748              
749             =item exclude_replies
750              
751             exclude_replies => 1
752              
753             If true, replies will be filtered from the results.
754              
755             =item exclude_rts
756              
757             exclude_rts => 1
758              
759             If true, RTs will be filtered from the results.
760              
761             =back
762              
763             =head2 post_tweet
764              
765             =head2 post_tweet_p
766              
767             my $tweet = $twitter->post_tweet($text, %options);
768             $twitter->post_tweet($text, %options, sub {
769             my ($twitter, $err, $tweet) = @_;
770             });
771             my $p = $twitter->post_tweet_p($text, %options);
772              
773             Post a status update (tweet) and retrieve the resulting
774             L. Requires OAuth 1.0 authentication. Accepts
775             the following options:
776              
777             =over
778              
779             =item in_reply_to_status_id
780              
781             in_reply_to_status_id => '12345'
782              
783             Indicates the tweet is in reply to an existing tweet ID. IDs should be
784             specified as a string to avoid issues with large integers. This parameter will
785             be ignored by the Twitter API unless the author of the referenced tweet is
786             mentioned within the status text as C<@username>.
787              
788             =item lat
789              
790             lat => '37.781157'
791              
792             The latitude of the location to attach to the tweet. This parameter will be
793             ignored by the Twitter API unless it is within the range C<-90.0> to C<90.0>,
794             and a corresponding C is specified. It is recommended to specify values
795             as strings to avoid issues with floating-point representations.
796              
797             =item long
798              
799             long => '-122.398720'
800              
801             The longitude of the location to attach to the tweet. This parameter will be
802             ignored by the Twitter API unless it is within the range C<-180.0> to C<180.0>,
803             and a corresponding C is specified. It is recommended to specify values as
804             strings to avoid issues with floating-point representations.
805              
806             =item place_id
807              
808             place_id => 'df51dec6f4ee2b2c'
809              
810             A Twitter L to attach to
811             the tweet.
812              
813             =item display_coordinates
814              
815             display_coordinates => 1
816              
817             If true, tweet will display the exact coordinates the tweet was sent from.
818              
819             =back
820              
821             =head2 retweet
822              
823             =head2 retweet_p
824              
825             my $tweet = $twitter->retweet($tweet_id);
826             $twitter->retweet($tweet_id, sub {
827             my ($twitter, $err, $tweet) = @_;
828             });
829             my $p = $twitter->retweet_p($tweet_id);
830              
831             Retweet the tweet ID or L object. Returns a
832             L representing the original tweet. Requires
833             OAuth 1.0 authentication.
834              
835             =head2 search_tweets
836              
837             =head2 search_tweets_p
838              
839             my $tweets = $twitter->search_tweets($query);
840             my $tweets = $twitter->search_tweets($query, %options);
841             $twitter->search_tweets($query, %options, sub {
842             my ($twitter, $err, $tweets) = @_;
843             });
844             my $p = $twitter->search_tweets_p($query, %options);
845              
846             Search Twitter and return a L of L
847             objects.
848              
849             Accepts the following options:
850              
851             =over
852              
853             =item geocode
854              
855             geocode => '37.781157,-122.398720,1mi'
856             geocode => ['37.781157','-122.398720','1mi']
857             geocode => {latitude => '37.781157', longitude => '-122.398720', radius => '1mi'}
858              
859             Restricts tweets to the given radius of the given latitude/longitude. Radius
860             must be specified as C (miles) or C (kilometers). It is recommended to
861             specify values as strings to avoid issues with floating-point representations.
862              
863             =item lang
864              
865             lang => 'eu'
866              
867             Restricts tweets to the given L
868             language code.
869              
870             =item result_type
871              
872             result_type => 'recent'
873              
874             Specifies what type of search results to receive. Valid values are C,
875             C, and C (default).
876              
877             =item count
878              
879             count => 5
880              
881             Limits the search results per page. Maximum C<100>, default C<15>.
882              
883             =item until
884              
885             until => '2015-07-19'
886              
887             Restricts tweets to those created before the given date, in the format
888             C.
889              
890             =item since_id
891              
892             since_id => '12345'
893              
894             Restricts results to those more recent than the given tweet ID. IDs should be
895             specified as a string to avoid issues with large integers. See
896             L
897             for more information on filtering results with C and C.
898              
899             =item max_id
900              
901             max_id => '54321'
902              
903             Restricts results to those older than (or equal to) the given tweet ID. IDs
904             should be specified as a string to avoid issues with large integers. See
905             L
906             for more information on filtering results with C and C.
907              
908             =back
909              
910             =head2 verify_credentials
911              
912             =head2 verify_credentials_p
913              
914             my $user = $twitter->verify_credentials;
915             $twitter->verify_credentials(sub {
916             my ($twitter, $error, $user) = @_;
917             });
918             my $p = $twitter->verify_credentials_p;
919              
920             Verify the authorizing user's credentials and return a representative
921             L object. Requires OAuth 1.0 authentication.
922              
923             =head1 BUGS
924              
925             Report any issues on the public bugtracker.
926              
927             =head1 AUTHOR
928              
929             Dan Book
930              
931             =head1 COPYRIGHT AND LICENSE
932              
933             This software is Copyright (c) 2015 by Dan Book.
934              
935             This is free software, licensed under:
936              
937             The Artistic License 2.0 (GPL Compatible)
938              
939             =head1 SEE ALSO
940              
941             L, L