The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Net::Twitter::Role::AppAuth;
$Net::Twitter::Role::AppAuth::VERSION = '4.01042';
use Moose::Role;
use Carp::Clan   qw/^(?:Net::Twitter|Moose|Class::MOP)/;
use HTTP::Request::Common qw/POST/;
use Net::Twitter::Types;

requires qw/_add_authorization_header ua from_json/;

use namespace::autoclean;

# flatten oauth_urls with defaults
around BUILDARGS => sub {
    my $orig  = shift;
    my $class = shift;

    my $args = $class->$orig(@_);
    my $oauth_urls = delete $args->{oauth_urls} || {
        request_token_url    => "https://api.twitter.com/oauth2/token",
        invalidate_token_url => "https://api.twitter.com/oauth2/invalidate_token",
    };

    return { %$oauth_urls, %$args };
};

has [ qw/consumer_key consumer_secret/ ] => (
    isa      => 'Str',
    is       => 'ro',
    required => 1,
);

# url attributes
has [ qw/request_token_url invalidate_token_url/ ] => (
    isa      => 'Net::Twitter::Types::URI',
    is       => 'ro',
    required => 1,
    coerce   => 1,
);

has access_token => (
    isa       => 'Str',
    is        => 'rw',
    clearer   => "clear_access_token",
    predicate => "authorized",
);

sub _add_consumer_auth_header {
    my ( $self, $req ) = @_;

    $req->headers->authorization_basic(
        $self->consumer_key, $self->consumer_secret);
}

sub request_access_token {
    my $self = shift;

    my $req = POST($self->request_token_url,
        'Content-Type' => 'application/x-www-form-urlencoded;charset=UTF-8',
        Content        => { grant_type => 'client_credentials' },
    );
    $self->_add_consumer_auth_header($req);

    my $res = $self->ua->request($req);
    croak "request_token failed: ${ \$res->code }: ${ \$res->message }"
        unless $res->is_success;

    my $r = $self->from_json($res->decoded_content);
    croak "unexpected token type: $$r{token_type}" unless $$r{token_type} eq 'bearer';

    return $self->access_token($$r{access_token});
}

sub invalidate_token {
    my $self = shift;

    croak "no access_token" unless $self->authorized;

    my $req = POST($self->invalidate_token_url,
        'Content-Type' => 'application/x-www-form-urlencoded;charset=UTF-8',
        Content        => join '=', access_token => $self->access_token,
    );
    $self->_add_consumer_auth_header($req);

    my $res = $self->ua->request($req);
    croak "invalidate_token failed: ${ \$res->code }: ${ \$res->message }"
        unless $res->is_success;

    $self->clear_access_token;
}

around _prepare_request => sub {
    my $orig = shift;
    my $self = shift;
    my ($http_method, $uri, $args, $authenticate) = @_;

    delete $args->{source};
    $self->$orig(@_);
};

override _add_authorization_header => sub {
    my ( $self, $msg ) = @_;

    return unless $self->authorized;

    $msg->header(authorization => join ' ', Bearer => $self->access_token);
};

1;

__END__

=encoding utf-8

=for stopwords

=head1 NAME

Net::Twitter::Role::AppAuth - OAuth2 Application Only Authentication

=head1 VERSION

version 4.01042

=head1 SYNOPSIS

  use Net::Twitter;

  my $nt = Net::Twitter->new(
      traits          => ['API::RESTv1_1', 'AppAuth'],
      consumer_key    => "YOUR-CONSUMER-KEY",
      consumer_secret => "YOUR-CONSUMER-SECRET",
  );

  $nt->request_token;

  my $tweets = $nt->user_timeline({ screen_name => 'Twitter' });

=head1 DESCRIPTION

Net::Twitter::Role::OAuth is a Net::Twitter role that provides OAuth
authentication instead of the default Basic Authentication.

Note that this client only works with APIs that are compatible to OAuth authentication.


=head1 METHODS

=over 4

=item authorized

True if the client has an access_token. This does not check the validity of the
access token, so requests may fail if it is invalid.

=item request_access_token

Request an access token. Returns the token as well as saving it in the object.

=item access_token

Get or set the access token.

=item invalidate_token

Invalidates and clears the access_token.

Note: There seems to be a Twitter bug preventing this from working---perhaps a
documentation bug. E.g., see: L<https://twittercommunity.com/t/revoke-an-access-token-programmatically-always-getting-a-403-forbidden/1902>

=back

=cut