File Coverage

blib/lib/Mojo/WebService/Twitter.pm
Criterion Covered Total %
statement 199 330 60.3
branch 48 148 32.4
condition 18 71 25.3
subroutine 43 81 53.0
pod 19 19 100.0
total 327 649 50.3


line stmt bran cond sub pod time code
1             package Mojo::WebService::Twitter;
2 2     2   1222514 use Mojo::Base -base;
  2         21  
  2         21  
3              
4 2     2   440 use Carp 'croak';
  2         5  
  2         97  
5 2     2   13 use Scalar::Util 'blessed';
  2         6  
  2         80  
6 2     2   10 use Mojo::Collection;
  2         4  
  2         68  
7 2     2   13 use Mojo::UserAgent;
  2         5  
  2         27  
8 2     2   88 use Mojo::Util qw(b64_encode encode url_escape);
  2         5  
  2         137  
9 2     2   973 use Mojo::WebService::Twitter::Error 'twitter_tx_error';
  2         5  
  2         104  
10 2     2   910 use Mojo::WebService::Twitter::Tweet;
  2         5  
  2         10  
11 2     2   70 use Mojo::WebService::Twitter::User;
  2         3  
  2         8  
12 2     2   46 use Mojo::WebService::Twitter::Util;
  2         4  
  2         62  
13 2     2   1113 use WWW::OAuth;
  2         32199  
  2         11707  
14              
15             our $VERSION = '1.001';
16              
17             has ['api_key','api_secret'];
18             has 'ua' => sub { Mojo::UserAgent->new };
19              
20             sub authentication {
21 11     11 1 1592 my $self = shift;
22 11 100 66     329 return $self->{authentication} // croak 'No authentication set' unless @_;
23 2         5 my $auth = shift;
24 2 50       19 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         6 $self->{authentication} = $self->_oauth2(@_);
38             } else {
39 0         0 croak "Unknown authentication $auth";
40             }
41 2         6 return $self;
42             }
43              
44             sub request_oauth {
45 1 50   1 1 277 my $cb = ref $_[-1] eq 'CODE' ? pop : undef;
46 1         3 my $self = shift;
47 1         4 my $tx = $self->_build_request_oauth(@_);
48 1 50       17 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         6 $tx = $self->ua->start($tx);
57 1 50       18373 die twitter_tx_error($tx) if $tx->error;
58 1   50     23 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     8 $url //= 'oob';
75 1         8 my $tx = $self->ua->build_tx(POST => _oauth_url('request_token'));
76 1         586 $self->_oauth->($tx->req, { oauth_callback => $url });
77 1         10079 return $tx;
78             }
79              
80             sub _from_request_oauth {
81 1     1   4 my ($self, $tx) = @_;
82 1         4 my $params = Mojo::Parameters->new($tx->res->text)->to_hash;
83             return undef unless $params->{oauth_callback_confirmed} eq 'true'
84 1 50 33     340 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         6 return $params;
87             }
88              
89             sub verify_oauth {
90 1 50   1 1 1770 my $cb = ref $_[-1] eq 'CODE' ? pop : undef;
91 1         4 my $self = shift;
92 1         5 my $tx = $self->_build_verify_oauth(@_);
93 1 50       16 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       8512 die twitter_tx_error($tx) if $tx->error;
103 1   50     23 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     10 $request_token_secret //= delete $self->{request_token_secrets}{$request_token} // croak "Unknown request token";
      33        
120 1         6 my $tx = $self->ua->build_tx(POST => _oauth_url('access_token'));
121 1         334 $self->_oauth($request_token, $request_token_secret)->($tx->req, { oauth_verifier => $verifier });
122 1         1501 return $tx;
123             }
124              
125             sub _from_verify_oauth {
126 1     1   4 my ($self, $tx) = @_;
127 1         6 my $params = Mojo::Parameters->new($tx->res->text)->to_hash;
128 1 50 33     239 return undef unless defined $params->{'oauth_token'} and defined $params->{'oauth_token_secret'};
129 1         7 return $params;
130             }
131              
132             sub request_oauth2 {
133 1 50   1 1 1649 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         3 $tx = $self->ua->start($tx);
145 1 50       19640 die twitter_tx_error($tx) if $tx->error;
146 1   50     24 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   3 my ($self) = @_;
162 1         4 my $tx = $self->ua->build_tx(POST => _oauth2_url('token'), form => { grant_type => 'client_credentials' });
163 1         820 $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     3 my $params = $tx->res->json // {};
170 1 50       260 return undef unless defined $params->{access_token};
171 1         5 return $params;
172             }
173              
174             sub get_tweet {
175 4 50   4 1 14618 my $cb = ref $_[-1] eq 'CODE' ? pop : undef;
176 4         10 my $self = shift;
177 4         21 my $tx = $self->_build_get_tweet(@_);
178 3 50       12 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         18 $tx = $self->ua->start($tx);
186 3 50       26921 die twitter_tx_error($tx) if $tx->error;
187 3         57 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   13 my ($self, $id) = @_;
203 4 50       15 croak 'Tweet ID is required for get_tweet' unless defined $id;
204 4 50       30 croak 'Invalid tweet ID to retrieve' unless $id =~ m/\A[0-9]+\z/;
205 4         35 my $tx = $self->ua->build_tx(GET => _api_url('statuses/show.json')
206             ->query(_www_form_urlencode(id => $id, tweet_mode => 'extended')));
207 4         865 $self->authentication->($tx->req);
208 3         80 return $tx;
209             }
210              
211             sub get_user {
212 2 50   2 1 3327 my $cb = ref $_[-1] eq 'CODE' ? pop : undef;
213 2         4 my $self = shift;
214 2         10 my $tx = $self->_build_get_user(@_);
215 2 50       8 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         7 $tx = $self->ua->start($tx);
223 2 50       17722 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   13 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       6 croak 'user_id or screen_name is required for get_user' unless %query;
244 2         6 $query{tweet_mode} = 'extended';
245 2         19 my $tx = $self->ua->build_tx(GET => _api_url('users/show.json')->query(_www_form_urlencode(%query)));
246 2         324 $self->authentication->($tx->req);
247 2         34 return $tx;
248             }
249              
250             sub post_tweet {
251 1 50   1 1 879 my $cb = ref $_[-1] eq 'CODE' ? pop : undef;
252 1         3 my $self = shift;
253 1         7 my $tx = $self->_build_post_tweet(@_);
254 1 50       4 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 $self->$cb(undef, _tweet_object($tx->res->json));
259 0         0 });
260             } else {
261 1         5 $tx = $self->ua->start($tx);
262 1 50       8575 die twitter_tx_error($tx) if $tx->error;
263 1         22 return _tweet_object($tx->res->json);
264             }
265             }
266              
267             sub post_tweet_p {
268 0     0 1 0 my $self = shift;
269 0         0 my $tx = $self->_build_post_tweet(@_);
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 return _tweet_object($tx->res->json);
274 0     0   0 }, sub { die Mojo::WebService::Twitter::Error->new(connection_error => $_[0]) });
  0         0  
275             }
276              
277             sub _build_post_tweet {
278 1     1   4 my ($self, $status, %params) = @_;
279 1 50       4 croak 'Status text is required to post a tweet' unless defined $status;
280 1         3 my %form;
281 1         3 $form{status} = $status;
282 1         4 $form{$_} = $params{$_} for grep { defined $params{$_} }
  4         11  
283             qw(in_reply_to_status_id lat long place_id);
284 1 0       2 $form{$_} = $params{$_} ? 'true' : 'false' for grep { defined $params{$_} }
  1         4  
285             qw(display_coordinates);
286 1         3 $form{tweet_mode} = 'extended';
287 1         5 my $tx = $self->ua->build_tx(POST => _api_url('statuses/update.json'),
288             {'Content-Type' => 'application/x-www-form-urlencoded'}, _www_form_urlencode(%form));
289 1         192 $self->authentication->($tx->req);
290 1         1647 return $tx;
291             }
292              
293             sub retweet {
294 0 0   0 1 0 my $cb = ref $_[-1] eq 'CODE' ? pop : undef;
295 0         0 my $self = shift;
296 0         0 my $tx = $self->_build_retweet(@_);
297 0 0       0 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 0         0 $tx = $self->ua->start($tx);
305 0 0       0 die twitter_tx_error($tx) if $tx->error;
306 0         0 return _tweet_object($tx->res->json);
307             }
308             }
309              
310             sub retweet_p {
311 0     0 1 0 my $self = shift;
312 0         0 my $tx = $self->_build_retweet(@_);
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_retweet {
321 0     0   0 my ($self, $id) = @_;
322 0 0       0 croak 'Tweet ID is required for retweet' unless defined $id;
323 0 0 0     0 $id = $id->id if blessed $id and $id->isa('Mojo::WebService::Twitter::Tweet');
324 0 0       0 croak 'Invalid tweet ID to retweet' unless $id =~ m/\A[0-9]+\z/;
325 0         0 my $tx = $self->ua->build_tx(POST => _api_url("statuses/retweet/$id.json")->query(tweet_mode => 'extended'));
326 0         0 $self->authentication->($tx->req);
327 0         0 return $tx;
328             }
329              
330             sub search_tweets {
331 1 50   1 1 5483 my $cb = ref $_[-1] eq 'CODE' ? pop : undef;
332 1         3 my $self = shift;
333 1         6 my $tx = $self->_build_search_tweets(@_);
334 1 50       5 if ($cb) {
335             $self->ua->start($tx, sub {
336 0     0   0 my ($ua, $tx) = @_;
337 0 0       0 return $self->$cb(twitter_tx_error($tx)) if $tx->error;
338 0   0     0 $self->$cb(undef, Mojo::Collection->new(map { _tweet_object($_) } @{$tx->res->json->{statuses} // []}));
  0         0  
  0         0  
339 0         0 });
340             } else {
341 1         5 $tx = $self->ua->start($tx);
342 1 50       10315 die twitter_tx_error($tx) if $tx->error;
343 1   50     19 return Mojo::Collection->new(map { _tweet_object($_) } @{$tx->res->json->{statuses} // []});
  15         91037  
  1         4  
344             }
345             }
346              
347             sub search_tweets_p {
348 0     0 1 0 my $self = shift;
349 0         0 my $tx = $self->_build_search_tweets(@_);
350             return $self->ua->start_p($tx)->then(sub {
351 0     0   0 my ($tx) = @_;
352 0 0       0 die twitter_tx_error($tx) if $tx->error;
353 0   0     0 return Mojo::Collection->new(map { _tweet_object($_) } @{$tx->res->json->{statuses} // []});
  0         0  
  0         0  
354 0     0   0 }, sub { die Mojo::WebService::Twitter::Error->new(connection_error => $_[0]) });
  0         0  
355             }
356              
357             sub _build_search_tweets {
358 1     1   3 my ($self, $q, %params) = @_;
359 1 50       4 croak 'Search query is required for search_tweets' unless defined $q;
360 1         3 my %query;
361 1         4 $query{q} = $q;
362 1         3 my $geocode = $params{geocode};
363 1 50       4 if (ref $geocode) {
364 0         0 my ($lat, $long, $rad);
365 0 0       0 ($lat, $long, $rad) = @$geocode if ref $geocode eq 'ARRAY';
366 0 0       0 ($lat, $long, $rad) = @{$geocode}{'latitude','longitude','radius'} if ref $geocode eq 'HASH';
  0         0  
367 0 0 0     0 $geocode = "$lat,$long,$rad" if defined $lat and defined $long and defined $rad;
      0        
368             }
369 1 50       4 $query{geocode} = $geocode if defined $geocode;
370 1         4 $query{$_} = $params{$_} for grep { defined $params{$_} } qw(lang result_type count until since_id max_id);
  6         15  
371 1         4 $query{tweet_mode} = 'extended';
372 1         7 my $tx = $self->ua->build_tx(GET => _api_url('search/tweets.json')->query(_www_form_urlencode(%query)));
373 1         191 $self->authentication->($tx->req);
374 1         17 return $tx;
375             }
376              
377             sub verify_credentials {
378 1 50   1 1 11 my $cb = ref $_[-1] eq 'CODE' ? pop : undef;
379 1         3 my $self = shift;
380 1         7 my $tx = $self->_build_verify_credentials(@_);
381 1 50       5 if ($cb) {
382             $self->ua->start($tx, sub {
383 0     0   0 my ($ua, $tx) = @_;
384 0 0       0 return $self->$cb(twitter_tx_error($tx)) if $tx->error;
385 0         0 $self->$cb(undef, Mojo::WebService::Twitter::User->new(twitter => $self)->from_source($tx->res->json));
386 0         0 });
387             } else {
388 1         4 $tx = $self->ua->start($tx);
389 1 50       9102 die twitter_tx_error($tx) if $tx->error;
390 1         29 return Mojo::WebService::Twitter::User->new(twitter => $self)->from_source($tx->res->json);
391             }
392             }
393              
394             sub verify_credentials_p {
395 0     0 1 0 my $self = shift;
396 0         0 my $tx = $self->_build_verify_credentials(@_);
397             return $self->ua->start_p($tx)->then(sub {
398 0     0   0 my ($tx) = @_;
399 0 0       0 die twitter_tx_error($tx) if $tx->error;
400 0         0 return Mojo::WebService::Twitter::User->new(twitter => $self)->from_source($tx->res->json);
401 0     0   0 }, sub { die Mojo::WebService::Twitter::Error->new(connection_error => $_[0]) });
  0         0  
402             }
403              
404             sub _build_verify_credentials {
405 1     1   27 my ($self) = @_;
406 1         5 my $tx = $self->ua->build_tx(GET => _api_url('account/verify_credentials.json')->query(tweet_mode => 'extended'));
407 1         374 $self->authentication->($tx->req);
408 1         1676 return $tx;
409             }
410              
411 9     9   94 sub _api_url { Mojo::WebService::Twitter::Util::_api_url(@_) }
412 2     2   23 sub _oauth_url { Mojo::WebService::Twitter::Util::_oauth_url(@_) }
413 1     1   9 sub _oauth2_url { Mojo::WebService::Twitter::Util::_oauth2_url(@_) }
414              
415             sub _oauth {
416 3     3   52 my $self = shift;
417 3         15 my ($api_key, $api_secret) = ($self->api_key, $self->api_secret);
418 3 50 33     39 croak 'Twitter API key and secret are required' unless defined $api_key and defined $api_secret;
419 3         8 my ($token, $token_secret) = @_;
420 3         31 my $oauth = WWW::OAuth->new(
421             client_id => $api_key,
422             client_secret => $api_secret,
423             token => $token,
424             token_secret => $token_secret,
425             );
426 3     4   247 return sub { $oauth->authenticate(@_) };
  4         19  
427             }
428              
429             sub _oauth2_request {
430 1     1   7 my $self = shift;
431 1         4 my ($api_key, $api_secret) = ($self->api_key, $self->api_secret);
432 1 50 33     14 croak 'Twitter API key and secret are required' unless defined $api_key and defined $api_secret;
433 1         4 my $token = b64_encode(url_escape($api_key) . ':' . url_escape($api_secret), '');
434 1     1   28 return sub { shift->headers->authorization("Basic $token") };
  1         4  
435             }
436              
437             sub _oauth2 {
438 1     1   3 my $self = shift;
439 1   33     4 my $token = shift // croak 'Access token is required for OAuth2 authentication';
440 1     6   8 return sub { shift->headers->authorization("Bearer $token") };
  6         30  
441             }
442              
443 19     19   24383 sub _tweet_object { Mojo::WebService::Twitter::Tweet->new->from_source(shift) }
444              
445 2     2   16784 sub _user_object { Mojo::WebService::Twitter::User->new->from_source(shift) }
446              
447             sub _www_form_urlencode {
448 8     8   2096 my @params = @_;
449 8         22 my @terms;
450 8         32 while (@params) {
451 16         161 my ($key, $values) = splice @params, 0, 2;
452 16 50       57 foreach my $value (ref $values eq 'ARRAY' ? @$values : $values) {
453 16   50     65 push @terms, join '=', map { url_escape encode 'UTF-8', $_ } ($key // ''), ($value // '');
  32   50     304  
454             }
455             }
456 8         166 return join '&', @terms;
457             }
458              
459             1;
460              
461             =head1 NAME
462              
463             Mojo::WebService::Twitter - Simple Twitter API client
464              
465             =head1 SYNOPSIS
466              
467             my $twitter = Mojo::WebService::Twitter->new(api_key => $api_key, api_secret => $api_secret);
468            
469             # Request and store access token
470             $twitter->authentication($twitter->request_oauth2);
471            
472             # Blocking API request
473             my $user = $twitter->get_user(screen_name => $name);
474             say $user->screen_name . ' was created on ' . $user->created_at->ymd;
475            
476             # Non-blocking API request
477             $twitter->get_tweet($tweet_id, sub {
478             my ($twitter, $err, $tweet) = @_;
479             print $err ? "Error: $err" : 'Tweet: ' . $tweet->text . "\n";
480             });
481             Mojo::IOLoop->start unless Mojo::IOLoop->is_running;
482            
483             # Non-blocking API request using promises
484             $twitter->get_tweet_p($tweet_id)->then(sub {
485             my ($tweet) = @_;
486             print 'Tweet: ' . $tweet->text . "\n";
487             })->catch(sub {
488             my ($err) = @_;
489             print "Error: $err";
490             })->wait;
491            
492             # Some requests require authentication on behalf of a user
493             $twitter->authentication(oauth => $token, $secret);
494             my $authorizing_user = $twitter->verify_credentials;
495            
496             my $new_tweet = $twitter->post_tweet('Something new and exciting!');
497              
498             =head1 DESCRIPTION
499              
500             L is a L based
501             L API client that can perform requests
502             synchronously or asynchronously. An API key and secret for a
503             L are required.
504              
505             API requests are authenticated by the L coderef, which can
506             either use an OAuth 2.0 access token to authenticate requests on behalf of the
507             application itself, or OAuth 1.0 credentials (access token and secret) to
508             authenticate requests on behalf of a specific user. The L
509             script can be used to obtain Twitter OAuth credentials for a user from the
510             command-line. A web application may wish to implement its own OAuth
511             authorization flow, passing a callback URL back to the application in
512             L, then calling L with the passed verifier
513             code to retrieve the credentials. See the
514             L for
515             more details.
516              
517             All methods which query the Twitter API can be called with an optional trailing
518             callback argument to run a non-blocking API query. Alternatively, the C<_p>
519             variant will run a non-blocking API query and return a L, which
520             can simplify complex sequences of non-blocking queries. On connection, HTTP, or
521             API error, blocking API queries will throw a L
522             exception. Non-blocking API queries will pass this exception object to the
523             callback or reject the promise, and otherwise pass the results to the callback
524             or resolve the promise.
525              
526             Note that this distribution implements only a subset of the Twitter API.
527             Additional features may be added as requested. See L for a more
528             fully-featured lightweight and modern twitter client library.
529              
530             =head1 ATTRIBUTES
531              
532             L implements the following attributes.
533              
534             =head2 api_key
535              
536             my $api_key = $twitter->api_key;
537             $twitter = $twitter->api_key($api_key);
538              
539             API key for your L.
540              
541             =head2 api_secret
542              
543             my $api_secret = $twitter->api_secret;
544             $twitter = $twitter->api_secret($api_secret);
545              
546             API secret for your L.
547              
548             =head2 ua
549              
550             my $ua = $webservice->ua;
551             $webservice = $webservice->ua(Mojo::UserAgent->new);
552              
553             HTTP user agent object to use for synchronous and asynchronous requests,
554             defaults to a L object.
555              
556             =head1 METHODS
557              
558             L inherits all methods from L, and
559             implements the following new ones.
560              
561             =head2 authentication
562              
563             my $code = $twitter->authentication;
564             $twitter = $twitter->authentication($code);
565             $twitter = $twitter->authentication({oauth_token => $access_token, oauth_token_secret => $access_token_secret});
566             $twitter = $twitter->authentication(oauth => $access_token, $access_token_secret);
567             $twitter = $twitter->authentication({access_token => $access_token});
568             $twitter = $twitter->authentication(oauth2 => $access_token);
569              
570             Get or set coderef used to authenticate API requests. Passing C with
571             optional token and secret, or a hashref containing C and
572             C, will set a coderef which uses a L to
573             authenticate requests. Passing C with required token or a hashref
574             containing C will set a coderef which authenticates using the
575             passed access token. The coderef will receive the L
576             object as the first parameter, and an optional hashref of C parameters.
577              
578             =head2 request_oauth
579              
580             =head2 request_oauth_p
581              
582             my $res = $twitter->request_oauth;
583             my $res = $twitter->request_oauth($callback_url);
584             $twitter->request_oauth(sub {
585             my ($twitter, $error, $res) = @_;
586             });
587             my $p = $twitter->request_oauth_p;
588              
589             Send an OAuth 1.0 authorization request and return a hashref containing
590             C and C (request token and secret). An
591             optional OAuth callback URL may be passed; by default, C is passed to use
592             PIN-based authorization. The user should be directed to the authorization URL
593             which can be retrieved by passing the request token to
594             L. After
595             authorization, the user will either be redirected to the callback URL with the
596             query parameter C, or receive a PIN to return to the
597             application. Either the verifier string or PIN should be passed to
598             L to retrieve an access token and secret.
599              
600             =head2 verify_oauth
601              
602             =head2 verify_oauth_p
603              
604             my $res = $twitter->verify_oauth($verifier, $request_token, $request_token_secret);
605             $twitter->verify_oauth($verifier, $request_token, $request_token_secret, sub {
606             my ($twitter, $error, $res) = @_;
607             });
608             my $p = $twitter->verify_oauth_p($verifier, $request_token, $request_token_secret);
609              
610             Verify an OAuth 1.0 authorization request with the verifier string or PIN from
611             the authorizing user, and the previously obtained request token and secret. The
612             secret is cached by L and may be omitted. Returns a hashref
613             containing C and C (access token and secret)
614             which may be passed directly to L to authenticate requests
615             on behalf of the user.
616              
617             =head2 request_oauth2
618              
619             =head2 request_oauth2_p
620              
621             my $res = $twitter->request_oauth2;
622             $twitter->request_oauth2(sub {
623             my ($twitter, $error, $res) = @_;
624             });
625             my $p = $twitter->request_oauth2_p;
626              
627             Request OAuth 2 credentials and return a hashref containing an C
628             that can be passed directly to L to authenticate requests on
629             behalf of the application itself.
630              
631             =head2 get_tweet
632              
633             =head2 get_tweet_p
634              
635             my $tweet = $twitter->get_tweet($tweet_id);
636             $twitter->get_tweet($tweet_id, sub {
637             my ($twitter, $err, $tweet) = @_;
638             });
639             $twitter->get_tweet_p($tweet_id);
640              
641             Retrieve a L by tweet ID.
642              
643             =head2 get_user
644              
645             =head2 get_user_p
646              
647             my $user = $twitter->get_user(user_id => $user_id);
648             my $user = $twitter->get_user(screen_name => $screen_name);
649             $twitter->get_user(screen_name => $screen_name, sub {
650             my ($twitter, $err, $user) = @_;
651             });
652             my $p = $twitter->get_user_p(user_id => $user_id);
653              
654             Retrieve a L by user ID or screen name.
655              
656             =head2 post_tweet
657              
658             =head2 post_tweet_p
659              
660             my $tweet = $twitter->post_tweet($text, %options);
661             $twitter->post_tweet($text, %options, sub {
662             my ($twitter, $err, $tweet) = @_;
663             });
664             my $p = $twitter->post_tweet_p($text, %options);
665              
666             Post a status update (tweet) and retrieve the resulting
667             L. Requires OAuth 1.0 authentication. Accepts
668             the following options:
669              
670             =over
671              
672             =item in_reply_to_status_id
673              
674             in_reply_to_status_id => '12345'
675              
676             Indicates the tweet is in reply to an existing tweet ID. IDs should be
677             specified as a string to avoid issues with large integers. This parameter will
678             be ignored by the Twitter API unless the author of the referenced tweet is
679             mentioned within the status text as C<@username>.
680              
681             =item lat
682              
683             lat => '37.781157'
684              
685             The latitude of the location to attach to the tweet. This parameter will be
686             ignored by the Twitter API unless it is within the range C<-90.0> to C<90.0>,
687             and a corresponding C is specified. It is recommended to specify values
688             as strings to avoid issues with floating-point representations.
689              
690             =item long
691              
692             long => '-122.398720'
693              
694             The longitude of the location to attach to the tweet. This parameter will be
695             ignored by the Twitter API unless it is within the range C<-180.0> to C<180.0>,
696             and a corresponding C is specified. It is recommended to specify values as
697             strings to avoid issues with floating-point representations.
698              
699             =item place_id
700              
701             place_id => 'df51dec6f4ee2b2c'
702              
703             A Twitter L to attach to
704             the tweet.
705              
706             =item display_coordinates
707              
708             display_coordinates => 1
709              
710             If true, tweet will display the exact coordinates the tweet was sent from.
711              
712             =back
713              
714             =head2 retweet
715              
716             =head2 retweet_p
717              
718             my $tweet = $twitter->retweet($tweet_id);
719             $twitter->retweet($tweet_id, sub {
720             my ($twitter, $err, $tweet) = @_;
721             });
722             my $p = $twitter->retweet_p($tweet_id);
723              
724             Retweet the tweet ID or L object. Returns a
725             L representing the original tweet. Requires
726             OAuth 1.0 authentication.
727              
728             =head2 search_tweets
729              
730             =head2 search_tweets_p
731              
732             my $tweets = $twitter->search_tweets($query);
733             my $tweets = $twitter->search_tweets($query, %options);
734             $twitter->search_tweets($query, %options, sub {
735             my ($twitter, $err, $tweets) = @_;
736             });
737             my $p = $twitter->search_tweets_p($query, %options);
738              
739             Search Twitter and return a L of L
740             objects. Accepts the following options:
741              
742             =over
743              
744             =item geocode
745              
746             geocode => '37.781157,-122.398720,1mi'
747             geocode => ['37.781157','-122.398720','1mi']
748             geocode => {latitude => '37.781157', longitude => '-122.398720', radius => '1mi'}
749              
750             Restricts tweets to the given radius of the given latitude/longitude. Radius
751             must be specified as C (miles) or C (kilometers). It is recommended to
752             specify values as strings to avoid issues with floating-point representations.
753              
754             =item lang
755              
756             lang => 'eu'
757              
758             Restricts tweets to the given L
759             language code.
760              
761             =item result_type
762              
763             result_type => 'recent'
764              
765             Specifies what type of search results to receive. Valid values are C,
766             C, and C (default).
767              
768             =item count
769              
770             count => 5
771              
772             Limits the search results per page. Maximum C<100>, default C<15>.
773              
774             =item until
775              
776             until => '2015-07-19'
777              
778             Restricts tweets to those created before the given date, in the format
779             C.
780              
781             =item since_id
782              
783             since_id => '12345'
784              
785             Restricts results to those more recent than the given tweet ID. IDs should be
786             specified as a string to avoid issues with large integers. See
787             L for more information on
788             filtering results with C and C.
789              
790             =item max_id
791              
792             max_id => '54321'
793              
794             Restricts results to those older than (or equal to) the given tweet ID. IDs
795             should be specified as a string to avoid issues with large integers. See
796             L for more information on
797             filtering results with C and C.
798              
799             =back
800              
801             =head2 verify_credentials
802              
803             =head2 verify_credentials_p
804              
805             my $user = $twitter->verify_credentials;
806             $twitter->verify_credentials(sub {
807             my ($twitter, $error, $user) = @_;
808             });
809             my $p = $twitter->verify_credentials_p;
810              
811             Verify the authorizing user's credentials and return a representative
812             L object. Requires OAuth 1.0 authentication.
813              
814             =head1 BUGS
815              
816             Report any issues on the public bugtracker.
817              
818             =head1 AUTHOR
819              
820             Dan Book
821              
822             =head1 COPYRIGHT AND LICENSE
823              
824             This software is Copyright (c) 2015 by Dan Book.
825              
826             This is free software, licensed under:
827              
828             The Artistic License 2.0 (GPL Compatible)
829              
830             =head1 SEE ALSO
831              
832             L, L