Revision 513 (by ahitrov, 2015/09/22 12:02:55) OAUTH Vkontakte: get user e-mail and authorize user by e-mail if applicable

package session::AUTH::VKontakte;

use strict;
use warnings;
use LWP::UserAgent;
use JSON::XS;
use Data::Dumper;
use URI;
use URI::QueryParam;
use Encode;
use Contenido::Globals;

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

=for rem
    vkontakte:
	auto_create_user: 1
	app_id: decimal digits
	app_secret: 32 hex digits
	authorize_url: http://api.vkontakte.ru/oauth/authorize
	access_token_url: https://api.vkontakte.ru/oauth/access_token
	user_info_url: https://api.vkontakte.ru/method/account.getProfileInfo
	user_post_url: ~
=cut

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

=for rem SCHEMA

$m->redirect ( $vk_connect->authorize_url( vk_redirect_uri => ... )->as_string );


=cut

our %SCOPE = (
	'notify'	=> 1,
	'friends'	=> 2,
	'photos'	=> 4,
	'audio'		=> 8,
	'video'		=> 16,
	'docs'		=> 131072,
	'notes'		=> 2048,
	'pages'		=> 128,
	'menu_link'	=> 256,
	'status'	=> 1024,
	'groups'	=> 262144,
	'email'		=> 4194304,
	'notifications'	=> 524288,
	'stats'		=> 1048576,
	'ads'		=> 32768,
	'offline'	=> 65536,
);

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

	$self->{vk_authorize_url}	= 'http://oauth.vk.com/authorize';
	$self->{vk_access_token_url}	= 'https://oauth.vk.com/access_token';
	$self->{vk_user_info_url}	= 'https://api.vk.com/method/getProfiles';

	for (qw( vk_app_id vk_app_secret vk_scope)) {
		$self->{$_} = $config{$_} || $state->{session}->{$_} || return undef;
	}
	$self->{timeout} = $state->{session}->{connection_timeout} || 3;
	for (qw(vk_user_post_url vk_redirect_uri)) {
		$self->{$_} = $config{$_} || $state->{session}->{$_};
	}
	return $self;
}

sub authorize_url {
	my $self = shift;
	my (%args) = @_;
	my $go = URI->new( $self->{vk_authorize_url} );
	$go->query_param( client_id	=> $self->{vk_app_id} );
	$go->query_param( scope		=> $self->{vk_scope} || '' );
	$go->query_param( response_type	=> 'code' );
	$args{redirect_uri} ||= $self->{vk_redirect_uri};
	for ( keys %args ) {
		$go->query_param( $_ => $args{$_} );
	}
	my $local_session = $session || $keeper->{session}->get_session;
	$local_session->set( vk_redirect_url => $self->{vk_redirect_uri} );
	return $go;
}

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

	my $local_session = $session || $keeper->{session}->get_session;
	my $redirect_uri = $self->{vk_redirect_uri};

	my $access_token = $local_session->{vk_access_token};
	my $vk_user_id = $local_session->{vk_user_id};
	my $expires = $local_session->{vk_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\n";
		return undef;
	}
	my $ua = LWP::UserAgent->new;
	$ua->timeout($self->{timeout});
	
	unless ($access_token) {
		my $req = URI->new( $self->{vk_access_token_url});
		$req->query_param( client_id		=> $self->{vk_app_id} );
		$req->query_param( client_secret	=> $self->{vk_app_secret} );
		$req->query_param( code			=> $code );
		$req->query_param( redirect_uri		=> $redirect_uri );
		warn "Token request: [$req]\n"			if $DEBUG;
		my $res = $ua->get($req);
		unless ($res->code == 200) {
			warn "VK: Access_token request failed: ".$res->status_line."\n";
			return undef;
		}
		my $info = $JSON->decode($res->content);
		warn Dumper $info				if $DEBUG;
		unless ( ref $info eq 'HASH' && ($access_token = $info->{access_token}) ) {
			warn "No access token in response: ".$res->content."\n";
			return undef;
		}
		$local_session->set( vk_access_token => $access_token );
		$local_session->set( vk_user_id => $info->{user_id} );
		if ( exists $info->{email} ) {
			$local_session->set( vk_email => $info->{email} );
			$local_session->set( email => $info->{email} )		unless exists $local_session->{email};
		}
		if ( my $expires = $info->{expires_in} ) {
			$local_session->set( vk_expires => time + $expires );
		} else {
			#$local_session->set( vk_expires => time + 3600*24 );
		}
		warn "VK: requested access token"		if $DEBUG;
	} else {
		warn "VK: have access token"			if $DEBUG;
	}

	my @fields = qw( uid first_name last_name nickname domain sex bdate city country timezone photo photo_medium photo_big );
	my $req = URI->new( $self->{vk_user_info_url} );
	$req->query_param( uid		=> $local_session->{vk_user_id} );
	$req->query_param( fields	=> join ',', @fields );
	$req->query_param( access_token	=> $access_token );

	warn "VK: Fetching user $req\n"				if $DEBUG;
	my $res = $ua->get($req);
	unless ($res->code == 200) {
		warn "VK: user request failed: ".$res->status_line."\n";
		return undef;
	}

	my $info;
	unless ( $info = eval { $JSON->decode($res->content) } ) {
		warn "user '".$res->content."' decode failed: $@\n";
		return undef;
	}
	warn Dumper($info)					if $DEBUG;
	return undef	unless exists $info->{response} && ref $info->{response} eq 'ARRAY' && @{$info->{response}};
	my $user_info = $info->{response}[0];
	foreach my $key ( qw(nickname last_name first_name) ) {
		$user_info->{$key} = Encode::encode('utf-8', $user_info->{$key});
	}

	my @plugins = split (/[\ |\t]+/, $state->{plugins});
	my $name = $user_info->{first_name}.' '.$user_info->{last_name};
	my $email = exists $user_info->{email} && $user_info->{email} ? $user_info->{email} : undef;
	if ( grep { $_ eq 'users' } @plugins ) {
		my $user;
		if ( $state->{users}->use_credentials ) {
			if ( $local_session->id ) {
				$user = $keeper->{users}->get_profile( id => $local_session->{id} );
			} else {
				$user = $keeper->{users}->get_profile( vkontakte => $user_info->{uid} );
			}
		}
		if ( !ref $user && (exists $local_session->{vk_email} || exists $local_session->{email}) ) {
			$user = $keeper->{users}->get_profile( email => exists $local_session->{vk_email} ? $local_session->{vk_email} : $local_session->{email} );
		}
		unless ( ref $user ) {
			$user = $keeper->{users}->get_profile( login => 'vkontakte:'.$user_info->{uid} );
		}
		unless ( ref $user ) {
			my $user_class = $state->{users}->profile_document_class;
			$user = $user_class->new( $keeper );
			my %props = map { $_->{attr} => $_ } $user->structure;
			$user->login( exists $local_session->{vk_email} ? $local_session->{vk_email} : 'vkontakte:'.$user_info->{uid} );
			$user->name( $user_info->{last_name}.', '.$user_info->{first_name} );
			$user->nickname( $user_info->{nickname} );
			$user->status( 1 );
			$user->type( 0 );
			$user->login_method('vkontakte');
			if ( exists $props{country} ) {
				$user->country( $user_info->{country} );
			}
			$user->email( $local_session->{vk_email} ? $local_session->{vk_email} : undef );

			my ($prop_ava) = grep { $_->{attr} eq 'avatar' && $_->{type} eq 'image' } $user->structure;
			if ( ref $prop_ava ) {
				my $avatar = $user->_store_image( $user_info->{photo_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' );
				unless ( ref $avatar && exists $avatar->{filename} ) {
					my $avatar = $user->_store_image( $user_info->{photo_big}, attr => 'avatar' );
					$user->avatar( $user->_serialize($avatar) );
					$user->store;
				}
			}
		}
		if ( $state->{users}->use_credentials ) {
			$user->create_credential(
				vkontakte => $user_info->{uid},
				avatar	=> $user_info->{photo_big},
			);
		}
		my %data = session::Keeper::_get_hash_from_profile( $user );
		$data{auth_by} = 'vkontakte';
		if ( $user_info->{photo} ) {
			$data{avatar} ||= $user_info->{photo};
		}
		$local_session->set( %data );

	} else {
		my %data = (
			id      => $user_info->{uid},
			name    => $name,
			nick    => $user_info->{nickname} || $name,
			login   => 'vkontakte:'.$user_info->{uid},
			status  => 1,
			type    => 0,
			auth_by	=> 'vkontakte',
			ltime   => time,
		);
		if ( $user_info->{photo} ) {
			$data{avatar} = $user_info->{photo};
		}
		if ( $user_info->{email} ) {
			$data{email} = $user_info->{email};
		}
		$local_session->set( %data );
	}
	return $local_session;
}

1;