?

Log in

No account? Create an account
Разбираю гуманитарные вопросы на винтики, функции, алгоритмы. Оживляю технологии
Совмещаю гуманитарный и технический пласты знаний в единое мировоззрение
VK::App - исправляем авторизованный доступ к ВК 
28th-Dec-2016 01:09 am
Не отследил момента когда авторизация посредством модуля VK::App перестала работать. Однако, поскольку использую данный модуль и иногда надо что-то узнать из-под своего пользователя, пришлось починять. Неавторизованное подключение к VK API работает нормально.

Авторизация производится при конструирование объекта, из метода new последовательно вызываются методы _login и _authorize_app. Этим реализуется схема получения ключей доступа «Authorization code flow».

И так, переработка:
sub _login
{
	my $self = shift;
	my $lpage = $self->{ua}->get(
		'https://oauth.vk.com/authorize?' .
		join('&',
			'client_id=' . $self->{api_id},
			'&display=mobile',
			'&client_secret=' . $self->{api_secret},
			'&response_type=code',
			'&redirect_uri=http://api.vk.com/blank.html',
			'&v=' . $self->{api_version}
		)
	);

	if ( ! $lpage->is_success )
	{
		return 0; # network problem?
	}
	my $action;
	if ( $lpage->content =~ m/action=\"(.+?)\"/ )
	{
		$action = $1;
	}

	my $loc;
	if ( $action )
	{
		if ( $self->{log} && ref($self->{log}) )
		{
			$self->{log}->debug( "action = $action" );
		}

		# Обработаем скрытые поля
		my @hidden = $lpage->content =~ m/(]+type="hidden"[^>]*>)/igs;
		my %hidden_pairs;
		foreach my $field ( @hidden )
		{
			my ($name) = $field =~ m/name="([^"]*)"/i;
			my ($value) = $field =~ m/value="([^"]*)"/i;
			if ( $name )
			{
				$hidden_pairs{$name} = $value || '';
			}
		}

		my $res = $self->{ua}->post($action, {
				email   => $self->{login},
				pass    => $self->{password},
				%hidden_pairs
			});

		if ( $res->status_line ne "302 Found" )
		{
			return 0;
		}
		if ( $res->header('location') !~ m/__q_hash/i )
		{
			return 0;
		}

		$loc = $res->header('location');
	}
	else
	{
		if ( $self->{log} && ref($self->{log}) )
		{
			$self->{log}->debug( __PACKAGE__ . "->_login abnormal" );
		}
		return 0;
	}

	my $res = $self->{ua}->get( $loc );

	if ( ! $res->is_success )
	{
		return 0;
	}

	# Если необходимо подтвердить доступ приложения, делаем это
	if ( $res->content =~ m/action=\"(.+?)\"/i )
	{
		$res = $self->{ua}->post( $1 );
		if ( $res->status_line ne "302 Found" )
		{
			return 0;
		}
		if ( $res->header('location') !~ m/code/ )
		{
			return 0;
		}

		($self->{code}) = $res->header('location') =~ m/code=([^&]+)/i;
	}
	elsif ( $res->status_line eq "302 Found" && $res->header('location') =~ m/code=([^&]+)/i )
	{
		$self->{code} = $1;
	}
	elsif ( $res->request->uri =~ m/code=([^&]+)/i )
	{
		$self->{code} = $1;
	}

	if ( ! $self->{code} )
	{
		return 0;
	}

	return $res->message;
} # sub _login

sub _authorize_app
{
	my $self = shift;
	push @{ $self->{ua}->requests_redirectable }, 'POST';
	my %authorize;
	$self->{code}           ||= '';
	$self->{api_secret}     ||= '';

	$authorize{request} = 'https://oauth.vk.com/access_token?' .
		'client_id=' . $self->{api_id} .
		'&client_secret=' . $self->{api_secret} .
		'&redirect_uri=http://api.vk.com/blank.html' .
		'&code=' . $self->{code};
	my $res = $self->{ua}->post( $authorize{request} );

	if ( ! $res->is_success )
	{
		return 0;
	}

	my $content = $res->decoded_content;

	my $decoded = eval {
		decode_json( $content );
	};

	if ( ! ( ref($decoded) && $decoded->{access_token} ) )
	{
		return 0;
	}

	$self->{access_token} = $decoded->{access_token};
	$self->{uid} = $decoded->{user_id};
	$self->{access_token_expires} = time() + $decoded->{expires_in};

	return $res->message;
} # sub _authorize_app

В методах используются два ключа, которые в стандартном VK::App нет.
1. $self->{log} - использую ссылку на объект Mojo::Log. При необходимости можно не использовать.
2. $self->{access_token_expires} - используется для переподключения из метода request:
	# Если протух access_token, надо перезапросить
	if ( $self->{access_token} && $self->{access_token_expires} && $self->{access_token_expires} <= time() )
	{
		die 'ERROR: login failed'
			unless( $self->_login() );
		die 'ERROR: authorize app failed'
			unless( $self->_authorize_app() );
	}


Дополнительное изменение, которое внес в модуль - это метод request_per_second, который реализует защиту от избыточного числа запросов в секунду. CK ограничивает 3 обращениями к API в секунду.
Метод request_per_second вызывается из основного метода request следующим образом:
if ( $self->{request_per_second} )
	{
		# Если задан параметр request_per_second, ожидаем заданное время
		$self->request_per_second;
	}


И сам метод:
sub request_per_second
{
	my $self = shift;
	my $currect_second = time();

	my $request = $self->{cache}->get("vk_request_counter");

	if ( ! ref($request) || $request->{currect_second} != $currect_second )
	{ # Если счетчик не задан или началась новая секунда - сбрасываем на значение по умолчанию
		$request = {
			currect_second  => $currect_second,
			counter         => 1
		};
		$self->{cache}->set("vk_request_counter", $request);

		return 1;
	}
	elsif ( $request->{counter} >= $self->{request_per_second} )
	{
		my $microseconds = (gettimeofday())[1];
		my $timeout = 1 - $microseconds / 1000000;
		select(undef, undef, undef, $timeout );
		$self->request_per_second;
	}

	$request->{counter}++;
	$self->{cache}->set("vk_request_counter", $request);
	return 1;
} # sub request_per_second

Где cache - объект самописного минималистского модуля кэширования на основе Redis. Это может быть любая общая для нескольких скриптов ячейка памяти.
Такой подход не идеальный. Правильнее было бы реализовать очередь с приоритетами, но дольше, а для моих целей вполне достаточно и такого подхода.
В методе используется библиотека Time::HiRes (метод gettimeofday).

На закуску пример скрипта получения комментариев поста на стене группы на основе слегка переработанного модуля VK::App.

This page was loaded Nov 20th 2017, 1:41 pm GMT.