Revision 243 (by ahitrov, 2012/09/13 18:29:46) oAuth: Facebook, Vkontakte, Mail.ru
package session::AUTH::Mailru;

use strict;
use warnings;
use LWP::UserAgent;
use JSON::XS;
use Data::Dumper;
use URI;
use URI::QueryParam;
use Encode;
use Digest::MD5 qw/ md5_hex /;
use Contenido::Globals;

use vars qw($VERSION);
$VERSION = '4.1';

=for rem
    facebook:
	auto_create_user: 1
	app_id: 15 decimal digits
	app_secret: 32 hex digits
	authorize_url: https://connect.mail.ru/oauth/authorize
	access_token_url: https://connect.mail.ru/oauth/token
	user_info_url: https://graph.facebook.com/me
	user_post_url: ~
	state: is passed back to your app as a parameter of the redirect_uri when the user completed the authentication
	store:
		class: "+Comments::Authentication::Store"
		type: facebook

=cut

our $JSON = JSON::XS->new->utf8;

=for rem SCHEMA

$m->redirect ( $fb_connect->authorize_url( mailru_redirect_uri => ... )->as_string );


=cut


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

	$self->{mailru_authorize_url}		= 'https://connect.mail.ru/oauth/authorize';
	$self->{mailru_access_token_url}	= 'https://connect.mail.ru/oauth/token';
	$self->{mailru_user_info_url}		= 'http://www.appsmail.ru/platform/api';

	for (qw(mailru_app_id mailru_app_secret)) {
		$self->{$_} = $config{$_} || $state->{session}{$_} || return undef;
	}
	$self->{timeout} = $state->{session}{connection_timeout} || 3;
	for (qw(mailru_user_post_url mailru_redirect_uri)) {
		$self->{$_} = $config{$_} || $state->{session}{$_};
	}
	return $self;
}

sub authorize_url {
	my $self = shift;
	my (%args) = @_;
	my $go = URI->new( $self->{mailru_authorize_url} );
	$go->query_param( client_id => $self->{mailru_app_id} );
	$go->query_param( response_type => "code" );
	$args{redirect_uri} ||= $self->{mailru_redirect_uri};
	for ( keys %args ) {
		$go->query_param( $_ => $args{$_} );
	}
	warn Dumper($go)					if $DEBUG;
	return $go;
}

sub authenticate {
	my ( $self, %authinfo ) = @_;
	warn "Mailru.authenticate"				if $DEBUG;

	my $local_session = $session || $keeper->{session}->get_session;
	my $redirect_uri = $self->{mailru_redirect_uri};
	
	my $access_token = $local_session->{mailru_access_token} || $local_session->{mailru_refresh_token};
	my $expires = $local_session->{mailru_expires};
	if ($access_token and $expires > time) {
		warn "Already have access_token"		if $DEBUG;
	} else {
		undef $access_token;
	}
	my $code = $authinfo{'code'};
	unless ( $code ) {
		warn "Call to authenticate without code";
		return undef;
	}
	my $ua = LWP::UserAgent->new;
	$ua->timeout($self->{timeout});
	unless ($access_token) {
		my $req = URI->new( $self->{mailru_access_token_url});
		$req->query_param( client_id	=> $self->{mailru_app_id} );
		$req->query_param( grant_type	=> 'authorization_code' );
		$req->query_param( redirect_uri	=> $redirect_uri );
		$req->query_param( client_secret=> $self->{mailru_app_secret} );
		$req->query_param( code		=> $code);
		warn "Post $req"				if $DEBUG;
		my $res = $ua->post($req);
		unless ($res->code == 200) {
			warn "access_token request failed: ".$res->status_line;
			return undef;
		}
		my $info = $JSON->decode($res->content);
		unless ( ref $info eq 'HASH' && ($access_token = $info->{access_token}) ) {
			warn "No access token in response: ".$res->content."\n";
			return undef;
		}
		$keeper->{session}->store_value( 
			mailru_access_token	=> $access_token, 
			mailru_refresh_token	=> $info->{refresh_token},
			mailru_id		=> $info->{x_mailru_vid},
		);
		$local_session->{mailru_access_token} = $access_token;
		$local_session->{mailru_refresh_token} = $info->{refresh_token};
		$local_session->{mailru_id} = $info->{x_mailru_vid};
		if( my $expires = $info->{expires_in} ) {
			$local_session->{mailru_expires} = time + $expires;
			$keeper->{session}->store_value( mailru_expires => $local_session->{mailru_expires} );
		} else {
			#$c->user_session->{'expires'} = time + 3600*24;
		}
		warn "Mailru: requested access token"		if $DEBUG;
	} else {
		warn "Mailru: have access token"		if $DEBUG;
	}
	
	my $req = URI->new( $self->{mailru_user_info_url} );
	my %params = (
		session_key => $access_token,
		app_id	=> $self->{mailru_app_id},
		method	=> 'users.getInfo',
		uids	=> $local_session->{mailru_id},
		secure	=> 1,
	);
	while ( my ($param, $value) = each %params ) {
		$req->query_param( $param => $value );
	}
	warn "SIG String: [".join ( '', map { $_.'='.$params{$_} } sort keys %params) . $self->{mailru_app_secret}."]\n";
	my $sig = md5_hex( join ( '', map { $_.'='.$params{$_} } sort keys %params) . $self->{mailru_app_secret} );
	$req->query_param( sig => $sig );
	
	warn "Fetching user $req"				if $DEBUG;
	my $res = $ua->get($req);
	unless ($res->code == 200) {
		warn "user request failed: ".$res->status_line;
		return undef;
	}
	my $info;
	unless ( $info = eval { $JSON->decode($res->content) } ) {
		warn "user '".$res->content."' decode failed: $@";
		return undef;
	}
	if ( ref $info eq 'ARRAY' && @$info ) {
		$info = $info->[0];
	} else {
		warn "Mailru: non-natural user info responce ".Dumper($info)."\n";
		return undef;
	}
	foreach my $key ( qw(nick last_name first_name) ) {
		$info->{$key} = Encode::encode('utf-8', $info->{$key});
		Encode::from_to( $info->{$key}, 'utf-8', 'koi8-r' );
	}
	warn "Userhash = ".Dumper($info)			if $DEBUG;
	#warn "facebook: user=$info->{name} / $info->{id} / $info->{gender}";

	$keeper->{session}->delete_key( 'mailru_redirect_url' );
	delete $local_session->{mailru_redirect_url};

	my @plugins = split (/[\ |\t]+/, $state->{plugins});
	my $name = $info->{first_name}.' '.$info->{last_name};
	if ( grep { $_ eq 'users' } @plugins ) {
		my $user = $keeper->{users}->get_profile( email => $info->{email} )	if $info->{email};
		$user ||= $keeper->{users}->get_profile( login => 'mailru:'.$info->{uid} );
		unless ( ref $user ) {
			my $user_class = $state->{users}->profile_document_class;
			$user = $user_class->new( $keeper );
			$user->login( $info->{email} || 'mailru:'.$info->{uid} );
			$user->name( $name );
			$user->nickname( $info->{nick} );
			$user->status( 1 );
			$user->type( 0 );
			$user->login_method('mailru');
			if ( $info->{location} ) {
				my $country = Encode::encode('utf-8', $info->{location}{country}{name});
				Encode::from_to( $country, 'utf-8', 'koi8-r' );
				$user->country( $country );
			}
			if ( $info->{birthday} && $info->{birthday} =~ /(\d{2})\.(\d{2})\.(\d{4})/ ) {
				$user->dtime( "$3-$2-$1" );
			}
			$user->email( $info->{email} || undef );

			my ($prop_ava) = grep { $_->{attr} eq 'avatar' && $_->{type} eq 'image' } $user->structure;
			if ( ref $prop_ava && $info->{pic_big} ) {
				my $avatar = $user->_store_image( $info->{pic_big}, attr => 'avatar' );
				$user->avatar( $user->_serialize($avatar) );
			}

			$user->store;
                } else {
			my ($prop_ava) = grep { $_->{attr} eq 'avatar' && $_->{type} eq 'image' } $user->structure;
			if ( ref $prop_ava ) {
				my $avatar = $user->get_image( 'avatar' );
				if ( $info->{pic_big} && !(ref $avatar && exists $avatar->{filename}) ) {
					my $avatar = $user->_store_image( $info->{pic_big}, attr => 'avatar' );
					$user->avatar( $user->_serialize($avatar) );
					$user->store;
				}
                        }
		}
		my %data = (
			id	=> $user->id,
			name	=> $user->name,
			nick	=> $user->nickname,
			login	=> $user->login,
			email	=> $user->email,
			status	=> $user->status,
			type	=> $user->type,
			ltime   => time,
			avatar	=> $info->{pic},
		);
		$keeper->{session}->store_value ( %data );
		while ( my ( $key, $value ) = each %data ) {
			$local_session->{$key} = $value;
		}
	} else {
		my %data = (
			id	=> $info->{uid},
			name	=> $name,
			nick	=> $info->{nick} || $name,
			email	=> $info->{email},
			login	=> $info->{email} || 'mailru:'.$info->{uid},
			status	=> 1,
			type	=> 0,
			auth_by	=> 'mailru',
			ltime	=> time,
		);
		if ( $user->{pic} ) {
			$data{avatar} = $info->{pic};
		}
		$keeper->{session}->store_value ( %data );
		while ( my ( $key, $value ) = each %data ) {
			$local_session->{$key} = $value;
		}
	}
	return $local_session;
}

1;