The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Catalyst::Authentication::Credential::Twitter;
BEGIN {
  $Catalyst::Authentication::Credential::Twitter::AUTHORITY = 'cpan:JESSESTAY';
}
# ABSTRACT:  Twitter authentication for Catalyst
$Catalyst::Authentication::Credential::Twitter::VERSION = '2.0.4';
use strict;
use warnings;
use base qw( Class::Accessor::Fast );
use Data::Dumper;

BEGIN {
    __PACKAGE__->mk_accessors(qw/
        _twitter callback_url consumer_key consumer_secret
    /);
}

use Catalyst::Exception ();
use Net::Twitter;

my $check_for_user_session;

sub twitter_user {
    my( $self, $c, $value ) = @_;

    $c->user_session->{twitter_user} = $value if $value;

    return $c->user_session->{twitter_user};
}

sub new {
    my ($class, $config, $c, $realm) = @_;
    my $self = {};
    bless $self, $class;

    die "context method 'user_session' not present. "
        ."Have you loaded Catalyst::Plugin::Session::PerUser ?" unless $c->can( 'user_session' );

    # Hack to make lookup of the configuration parameters less painful
    my $params = { %{ $config }, %{ $realm->{config} } };

	$params->{'consumer_key'} ||= $c->config->{'twitter_consumer_key'};
	$params->{'consumer_secret'} ||= $c->config->{'twitter_consumer_secret'};
	$params->{'callback_url'} ||= $c->config->{'twitter_callback_url'};
    # Check for required params (yes, nasty)
    for my $param (qw/consumer_key consumer_secret callback_url/) {
        $self->$param($params->{$param}) or
            Catalyst::Exception->throw("$param not defined") 
    }

    # Create a Net::Twitter instance
    $self->_twitter(Net::Twitter->new({ 
		'traits'        	=> ['API::RESTv1_1', 'OAuth'],
		'consumer_key' 		=> $self->consumer_key, 
        'consumer_secret'	=> $self->consumer_secret,
		'ssl'				=> 1,
	}));

    return $self;
}

sub authenticate_twitter {
    my ( $self, $c ) = @_;

	if (!$c->user_session->{'request_token'} || !$c->user_session->{'request_token_secret'} || !$c->req->params->{'oauth_verifier'}) {
        $c->log->debug('no request token present, or no verifier');
        return undef;
	}

	my $token = $c->user_session->{'request_token'};
	my $token_secret = $c->user_session->{'request_token_secret'};
	my $verifier = $c->req->params->{'oauth_verifier'};
	my $access_token = $c->user_session->{'access_token'};
	my $access_token_secret = $c->user_session->{'access_token_secret'};

    # Create a Net::Twitter instance
    $self->_twitter(Net::Twitter->new({
		'traits'        	=> ['API::RESTv1_1', 'OAuth'],
		'consumer_key' 		=> $self->consumer_key,
        'consumer_secret'	=> $self->consumer_secret,
		'ssl'				=> 1,
	}));

	if (!$access_token && !$access_token_secret) {
		$self->_twitter->request_token($token);
    	$self->_twitter->request_token_secret($token_secret);

		($access_token, $access_token_secret) = $self->_twitter->request_access_token('verifier' => $verifier);
		# this is in case we need to register the user after the oauth process
		$c->user_session->{'access_token'} = $access_token;
		$c->user_session->{'access_token_secret'} = $access_token_secret;
	}

	# get the user
	$self->_twitter->access_token($access_token);
    $self->_twitter->access_token_secret($access_token_secret);

	my $twitter_user_hash = eval {
		$self->_twitter->verify_credentials;
	};

	if ($@ || !$twitter_user_hash) {
		$c->log->debug("no twitter_user_hash or error: ".$@);
		return undef;
	}

	$twitter_user_hash->{'access_token'} = $access_token;
	$twitter_user_hash->{'access_token_secret'} = $access_token_secret;

    $self->twitter_user( $c, $twitter_user_hash );

    return $twitter_user_hash;
}

sub authenticate {
    my ( $self, $c, $realm, $authinfo ) = @_;

	unless ($authinfo) {
        $self->authenticate_twitter( $c ) unless $self->twitter_user($c);

        return undef unless $self->twitter_user($c);

		$authinfo = {
			'twitter_user_id'	=> $self->twitter_user($c)->{id},
            id                  => $self->twitter_user($c)->{id},
		};
	}

    my $user_obj = $realm->find_user($authinfo, $c);

    return undef unless ref $user_obj;

    eval { 
		if (   $user_obj->result_source->has_column('twitter_user') 
            && $user_obj->result_source->has_column('twitter_access_token') 
            && $user_obj->result_source->has_column('twitter_access_token_secret')) {
            my $twitter_user = $self->twitter_user($c);
			$user_obj->update({
				'twitter_user'					=> $twitter_user->{'screen_name'},
				'twitter_access_token'			=> $twitter_user->{access_token},
				'twitter_access_token_secret'	=> $twitter_user->{access_token_secret},
			});
		}
    };

    return $user_obj;

}

sub authenticate_twitter_url {
    my ($self, $c) = @_;

    # Create a Net::Twitter instance
    $self->_twitter(Net::Twitter->new(
		'traits'        	=> ['API::RESTv1_1', 'OAuth'],
		'consumer_key' 		=> $self->consumer_key,
        'consumer_secret'	=> $self->consumer_secret,
		'ssl'				=> 1,
	));

    my $uri = $self->_twitter->get_authentication_url( 'callback'	=> $c->config->{'twitter_callback_url'} || $self->callback_url );
	$c->user_session->{'request_token'} = $self->_twitter->request_token;
	$c->user_session->{'request_token_secret'} = $self->_twitter->request_token_secret;
	$c->user_session->{'access_token'} = '';
	$c->user_session->{'access_token_secret'} = '';

    return $uri;
}

1;

__END__

=pod

=encoding UTF-8

=head1 NAME

Catalyst::Authentication::Credential::Twitter - Twitter authentication for Catalyst

=head1 VERSION

version 2.0.4

=head1 SYNOPSIS

In MyApp.pm

 use Catalyst qw/
    Authentication
    Session
    Session::Store::FastMmap
    Session::State::Cookie
	Session::PerUser
 /;
 
 MyApp->config(
     "Plugin::Authentication" => {
         default_realm => "twitter",
         realms => {
             twitter => {
                 credential => {
                     class => "Twitter",
                 },

                 consumer_key    => 'twitter-consumer_key-here',
                 consumer_secret => 'twitter-secret-here',
                 callback_url => 'http://mysite.com/callback',
				 # you can bypass the above by including
				 # "twitter_consumer_key", "twitter_consumer_secret", 
				 # and "twitter_callback_url" in your Catalyst site
				 # configuration or yml file
             },
         },
     },
 );

And then in your Controller:

 sub login : Local {
    my ($self, $c) = @_;
    
    my $realm = $c->get_auth_realm('twitter');
    $c->res->redirect( $realm->credential->authenticate_twitter_url($c) );
 }

And finally the callback you specified in your API key request above (e.g.
example.com/twitter/callback/ ):

 sub callback : Local {
    my ($self, $c) = @_;
    
    if (my $user = $c->authenticate(undef,'twitter')) {
		# user has an account - redirect or do something cool
    	$c->res->redirect("/super/secret/member/area");
	}
	else {
		# user doesn't have an account - either detect Twitter
		# credentials and create one, or return an error.
		#
		# Note that "request_token" and "request_token_secret"
		# are stored in $c->user_session as hashref variables under
		# the same names
	}
 }

=head1 DESCRIPTION

This module handles Twitter API authentication in a Catalyst application.

Note that I<Catalyst::Authentication::Credential::Twitter> needs
the catalyst application to also load L<Catalyst::Plugin::Session::PerUser>
to be functional.

=head1 METHODS

As per guidelines of L<Catalyst::Plugin::Authentication>, there are two
mandatory methods, C<new> and C<authenticate>. Since this is not really
enough for the Twitter API, I've added one more.

=head2 new()

Will not be called by you directly, but will use the configuration you
provide (see above). Mandatory parameters are C<consumer_key>, C<consumer_secret> and
C<callback_url>. Note that you can also include C<twitter_consumer_key>, C<twitter_consumer_secret>, and C<twitter_callback_url> as variables in your Catalyst site configuration or yml file and you don't need to pass configuration parameters in your MyApp.pm file.  Please see L<Net::Twitter> for more details on them.

=head2 authenticate_twitter_url( $c )

This method will return the authentication URL. Bounce your users there
before calling the C<authentication> method.

=head2 authenticate( )

Handles the authentication. Nothing more, nothing less. It returns
a L<Catalyst::Authentication::User::Hash> with the following keys
(all coming straight from Twitter).

=over 4

=item twitter_user

=item twitter_user_id

=item twitter_access_token

=item twitter_access_token_secret

=back

Your database must at least contain a column called "twitter_user_id"
in your main user table. If the other keys are present they will be
updated on login with Twitter's most up-to-date information for that
user.

=head2 authenticate_twitter( )

Only performs the twitter authentication. Returns a hashref containing
the user's information given by Twitter (see C<authenticate()> above for
the lists of keys returned), or undef if the authentication failed.

=head2 twitter_user($c)

Contains the user's twitter information after a successful twitter
authentication via C<authenticate_twitter()> or
C<authenticate()>. Useful if, for example, you want to create users
on-the-fly:

    sub twitter_callback :Path( 'twitter/callback' ) {
        my ($self, $c) = @_;

        my $twitter = $c->get_auth_realm('twitter')->credential;
        my $user =  $twitter->authenticate( $c );

        # properly authenticated against twitter,
        # user just doesn't exist yet
        if ( !$user and  $twitter->twitter_user($c) ) {
            $user = $self->model->create_user( $twitter->twitter_user($c) );
        }

        # etc
    }

=head1 SEE ALSO

L<Catalyst::Plugin::Authentication>, L<Net::Twitter>

=head1 BUGS AND LIMITATIONS

C<Catalyst::Authentication::Credential::Twitter> works well 
with L<Catalyst::Authentication::Store::DBIx::Class>, but might 
have problem with other stores, as its C<authenticate()> method uses

    $realm->find_user({
        twitter_user_id => $authenticated_twitter_id
    }, $c);

to find the user. If this causes a problem for your store, 
you can get around it by using C<authenticate_twitter()> and
accessing the store manually.

Please report bugs to L<http://rt.cpan.org/Ticket/Create.html?Queue=Catalyst-Authentication-Credential-Twitter>

=head1 THANKS

Thanks go out Daisuke Murase for writing C::P::A::Credential::Flickr,
Marc Mims and Chris Thompson for Net::Twitter.

=head1 AUTHORS

=over 4

=item *

Jesse Stay <jesse@staynalive.com>

=item *

Yanick Champoux <yanick@cpan.org>

=back

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2009 by Jesse Stay.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut