The Perl and Raku Conference 2025: Greenville, South Carolina - June 27-29 Learn more

use strict;
use utf8;
use 5.008;
our $VERSION = '0.64';
use Carp;
use JSON;
use URI;
$Net::OAuth::PROTOCOL_VERSION = Net::OAuth::PROTOCOL_VERSION_1_0A;
our %PATH = (
);
our %RESOURCE_URL_BASE = (
);
sub new {
my ($class, %args) = @_;
$args{api_version} ||= '1.1';
$args{access_token} ||= $args{token};
$args{access_token_secret} ||= $args{token_secret};
my @required = qw(access_token access_token_secret consumer_key consumer_secret);
for my $item (@required) {
defined $args{$item} or Carp::croak "$item is required";
}
return bless { %args }, $class;
}
sub get {
my $cb = pop;
my ($self, $endpoint, $params) = @_;
my $type = $endpoint =~ /^http.+\.json$/ ? 'url' : 'api';
$self->request($type => $endpoint, method => 'GET', params => $params, $cb);
return $self;
}
sub post {
my ($self, $endpoint, $params, $cb) = @_;
my $type = $endpoint =~ /^http.+\.json$/ ? 'url' : 'api';
$self->request($type => $endpoint, method => 'POST', params => $params, $cb);
return $self;
}
sub request {
my $cb = pop;
my ($self, %opt) = @_;
($opt{api} || $opt{url})
or Carp::croak "'api' or 'url' option is required";
my $url_base = $RESOURCE_URL_BASE{ $self->{api_version} };
my $url = $opt{url} || sprintf $url_base, $opt{api};
ref $cb eq 'CODE'
or Carp::croak "callback coderef is required";
my $params = $opt{params} || {};
my $is_multipart = ref $params eq 'ARRAY';
my $method = uc $opt{method};
$method =~ /^(?:GET|POST)$/
or Carp::croak "'method' option should be GET or POST";
my $req = $self->_make_oauth_request(
class => 'Net::OAuth::ProtectedResourceRequest',
request_url => $url,
request_method => $method,
extra_params => ($is_multipart ? {} : $params),
consumer_key => $self->{consumer_key},
consumer_secret => $self->{consumer_secret},
token => $self->{access_token},
token_secret => $self->{access_token_secret},
);
my $req_params = {};
if ($method eq 'POST') {
$url = $req->normalized_request_url;
if ($is_multipart) {
my $encoded_params = Data::Recursive::Encode::_apply(
sub { utf8::is_utf8($_[0]) ? Encode::encode_utf8($_[0]) : $_[0] },
{},
$params
);
my $ireq = POST(
$url,
Content_Type => 'multipart/form-data',
Content => [ @$encoded_params ]
);
$req_params->{body} = $ireq->content;
$req_params->{headers} = {
Authorization => $req->to_authorization_header,
'Content-Type' => join "; ", $ireq->content_type,
};
} else {
$req_params->{body} = $req->to_post_body;
$req_params->{headers}{'Content-Type'} = 'application/x-www-form-urlencoded';
}
} else {
$url = $req->to_url;
}
AnyEvent::HTTP::http_request $method => $url, %$req_params, sub {
my ($body, $hdr) = @_;
local $@;
my $json = eval { JSON::decode_json($body) };
if ($hdr->{Status} =~ /^2/) {
$cb->($hdr, $json, $@ ? "parse error: $@" : $hdr->{Reason});
} else {
$cb->($hdr, undef, $hdr->{Reason}, $json);
}
};
return $self;
}
sub _make_oauth_request {
my $self = shift;
my %opt = @_;
my $class = delete $opt{class};
local $Net::OAuth::SKIP_UTF8_DOUBLE_ENCODE_CHECK = 1;
my $req = $class->new(
version => '1.0',
timestamp => time,
nonce => Digest::SHA::sha1_base64(time . $$ . rand),
signature_method => 'HMAC-SHA1',
%opt,
);
$req->sign;
return $req;
}
sub get_request_token {
my ($class, %args) = @_;
my @required = qw(consumer_key consumer_secret callback_url);
for my $item (@required) {
defined $args{$item} or Carp::croak "$item is required";
}
ref $args{cb} eq 'CODE'
or Carp::croak "cb must be callback coderef";
$args{auth} ||= 'authorize';
my $req = __PACKAGE__->_make_oauth_request(
class => 'Net::OAuth::RequestTokenRequest',
request_method => 'GET',
request_url => $PATH{request_token},
consumer_key => $args{consumer_key},
consumer_secret => $args{consumer_secret},
callback => $args{callback_url},
);
AnyEvent::HTTP::http_request GET => $req->to_url, sub {
my ($body, $header) = @_;
my %token = __PACKAGE__->_parse_response($body);
my $location = URI->new($PATH{ $args{auth} });
$location->query_form(%token);
$args{cb}->($location->as_string, \%token, $body, $header);
};
}
sub get_access_token {
my ($class, %args) = @_;
my @required = qw(
consumer_key consumer_secret
oauth_token oauth_token_secret oauth_verifier
);
for my $item (@required) {
defined $args{$item} or Carp::croak "$item is required";
}
ref $args{cb} eq 'CODE'
or Carp::croak "cb must be callback coderef";
my $req = __PACKAGE__->_make_oauth_request(
class => 'Net::OAuth::AccessTokenRequest',
request_method => 'GET',
request_url => $PATH{access_token},
consumer_key => $args{consumer_key},
consumer_secret => $args{consumer_secret},
token => $args{oauth_token},
token_secret => $args{oauth_token_secret},
verifier => $args{oauth_verifier},
);
AnyEvent::HTTP::http_request GET => $req->to_url, sub {
my ($body, $header) = @_;
my %response = __PACKAGE__->_parse_response($body);
$args{cb}->(\%response, $body, $header);
};
}
sub _parse_response {
my ($class, $body) = @_;
my %query;
for my $pair (split /&/, $body) {
my ($key, $value) = split /=/, $pair;
$query{$key} = URI::Escape::uri_unescape($value);
}
return %query;
}
sub parse_timestamp { # Twitter uses weird created_at format: "Thu Mar 01 17:38:56 +0000 2012"
my ($class, $created_at) = @_;
localtime( Time::Piece->strptime($created_at, '%a %b %d %H:%M:%S %z %Y' )->epoch )
}
1;
__END__
=encoding utf-8
=head1 NAME
AnyEvent::Twitter - A thin wrapper for Twitter API using OAuth
=head1 SYNOPSIS
use utf8;
use Data::Dumper;
use AnyEvent;
use AnyEvent::Twitter;
my $ua = AnyEvent::Twitter->new(
consumer_key => 'consumer_key',
consumer_secret => 'consumer_secret',
token => 'access_token',
token_secret => 'access_token_secret',
);
# or
my $ua = AnyEvent::Twitter->new(
consumer_key => 'consumer_key',
consumer_secret => 'consumer_secret',
access_token => 'access_token',
access_token_secret => 'access_token_secret',
);
# or, if you use eg/gen_token.pl, you can write simply as:
my $json_text = slurp 'config.json';
my $config = JSON::decode_json($json_text);
my $ua = AnyEvent::Twitter->new(%$config);
my $cv = AE::cv;
# GET request
$cv->begin;
$ua->get('account/verify_credentials', sub {
my ($header, $response, $reason) = @_;
say $response->{screen_name};
$cv->end;
});
# GET request with parameters
$cv->begin;
$ua->get('account/verify_credentials', {
include_entities => 1
}, sub {
my ($header, $response, $reason) = @_;
say $response->{screen_name};
$cv->end;
});
# POST request with parameters
$cv->begin;
$ua->post('statuses/update', {
status => 'いろはにほへと ちりぬるを'
}, sub {
my ($header, $response, $reason) = @_;
say $response->{user}{screen_name};
$cv->end;
});
# verbose and old style
$cv->begin;
$ua->request(
method => 'GET',
api => 'account/verify_credentials',
sub {
my ($hdr, $res, $reason) = @_;
if ($res) {
print "ratelimit-remaining : ", $hdr->{'x-ratelimit-remaining'}, "\n",
"x-ratelimit-reset : ", $hdr->{'x-ratelimit-reset'}, "\n",
"screen_name : ", $res->{screen_name}, "\n";
} else {
say $reason;
}
$cv->end;
}
);
$cv->begin;
$ua->request(
method => 'POST',
api => 'statuses/update',
params => { status => 'hello world!' },
sub {
print Dumper \@_;
$cv->end;
}
);
$cv->begin;
$ua->request(
method => 'POST',
params => { status => 'いろはにほへと ちりぬるを' },
sub {
print Dumper \@_;
$cv->end;
}
);
$cv->recv;
=head1 DESCRIPTION
AnyEvent::Twitter is a very thin wrapper for Twitter API using OAuth.
=head1 API VERSION
As of version 0.63, L<AnyEvent::Twitter> supports Twitter REST API v1.1.
NOTE: API version 1.0 is already deprecated.
=head1 METHODS
=head2 new
All arguments are required except C<api_version>.
If you don't know how to obtain these parameters, take a look at eg/gen_token.pl and run it.
=over 4
=item C<consumer_key>
=item C<consumer_secret>
=item C<access_token> (or C<token>)
=item C<access_token_secret> (or C<token_secret>)
=item C<api_version> (optional; default: 1.1)
If you have a problem with API changes, specify C<api_version> parameter.
Possible values are: C<1.1> or C<1.0>
=back
=head2 get
=over 4
=item C<< $ua->get($api, sub {}) >>
=item C<< $ua->get($api, \%params, sub {}) >>
=item C<< $ua->get($url, sub {}) >>
=item C<< $ua->get($url, \%params, sub {}) >>
=back
=head2 post
=over 4
=item C<< $ua->post($api, \%params, sub {}) >>
=item C<< $ua->post($url, \%params, sub {}) >>
=item C<< $ua->post($api, \@params, sub {}) >>
=item C<< $ua->post($url, \@params, sub {}) >>
=back
=head3 UPLOADING MEDIA FILE
You can use C<statuses/update_with_media> API to upload photos by specifying parameters as arrayref like below example.
Uploading photos will be tranferred with Content-Type C<multipart/form-data> (not C<application/x-www-form-urlencoded>)
use utf8;
$ua->post(
'statuses/update_with_media',
[
status => '桜',
'media[]' => [ undef, $filename, Content => $loaded_image_binary ],
],
sub {
my ($hdr, $res, $reason) = @_;
say $res->{user}{screen_name};
}
);
=head2 request
These parameters are required.
=over 4
=item C<api> or C<url>
The C<api> parameter is a shortcut option.
If you want to specify the API C<url>, the C<url> parameter is good for you. The format should be 'json'.
The C<api> parameter will be internally processed as:
sprintf 'https://api.twitter.com/1.1/%s.json', $api; # version 1.1
sprintf 'http://api.twitter.com/1/%s.json', $api; # version 1.0
You can find available C<api>s at L<API Documentation|https://dev.twitter.com/docs/api>
=item C<method> and C<params>
Investigate the HTTP method and required parameters of Twitter API that you want to use.
Then specify it. GET and POST methods are allowed. You can omit C<params> if Twitter API doesn't require it.
=item callback
This module is L<AnyEvent::HTTP> style, so you have to pass the callback (coderef).
Passed callback will be called with C<$header>, C<$response>, C<$reason> and C<$error_response>.
If something is wrong with the response from Twitter API, C<$response> will be C<undef>.
On non-2xx HTTP status code, you can get the decoded response via C<$error_response>.
So you can check the value like below.
my $callback = sub {
my ($header, $response, $reason, $error_response) = @_;
if ($response) {
say $response->{screen_name};
} else {
say $reason;
for my $error (@{$error_response->{errors}}) {
say "$error->{code}: $error->{message}";
}
}
};
=back
=head2 parse_timestamp
C<parse_timestamp> parses C<created_at> timestamp like "Thu Mar 01 17:38:56 +0000 2012".
It returns L<Time::Piece> object. Its timezone is localtime.
=over 4
=item C<< AnyEvent::Twitter->parse_timestamp($created_at) >>
=back
=head1 TESTS
Most of all tests are written as author tests since this module depends on remote API server.
So if you want read code that works well, take a look at C<xt/> directory.
=head1 EXPERIMENTAL METHODS
Methods listed below are experimental feature. So interfaces or returned values may vary in the future.
=head2 C<< AnyEvent::Twitter->get_request_token >>
AnyEvent::Twitter->get_request_token(
consumer_key => $consumer_key,
consumer_secret => $consumer_secret,
callback_url => 'http://example.com/callback',
# auth => 'authenticate',
cb => sub {
my ($location, $response, $body, $header) = @_;
# $location is the endpoint where users are asked the permission
# $response is a hashref of parsed body
# $body is raw response itself
# $header is response headers
},
);
=head2 C<< AnyEvent::Twitter->get_access_token >>
AnyEvent::Twitter->get_access_token(
consumer_key => $consumer_key,
consumer_secret => $consumer_secret,
oauth_token => $oauth_token,
oauth_token_secret => $oauth_token_secret,
oauth_verifier => $oauth_verifier,
cb => sub {
my ($token, $body, $header) = @_;
# $token is the parsed body
# $body is raw response
# $header is response headers
},
);
=head1 CONTRIBUTORS
=over 4
=item ramusara
He gave me plenty of test code.
=item Hideki Yamamura
He cleaned my code up.
=back
=head1 AUTHOR
punytan E<lt>punytan@gmail.comE<gt>
=head1 SEE ALSO
L<AnyEvent::HTTP>, L<Net::OAuth>
=head1 LICENSE
This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.
=cut