Близька до ідеалу адаптація ВКонтакте API для платформи .NET

Здрастуйте, дорогі Хабравчане!
Мало для кого є секретом, що за останні кілька років однойменна соціальна мережа встигла грунтовно увійти людям в звичку і вирости до масштабів сервісу континентального рівня.
За цей час простір ВКонтакте активно освоювали всі, хто побачив там якийсь потенціал, і сьогодні в ньому існує безліч проектів, націлених на аудиторію з різними уподобаннями.
Коли робота з інформацією набуває вирішальне значення, стає очевидно, що в прагненні до найкращим результатам люди потребуватимуть нових і не завжди стандартні рішення обробки даних.
Мене звати Ілля Терещук, на сьогоднішній день я живу веденням проекту в соціальній мережі і займаюся програмуванням. Створюючи своє перше додаток для роботи з API, я зіткнувся з чималою кількістю нюансів, які, по всій видимості, дають неабияк поламати голову кожному, хто береться за подібне вперше.
У розробці фундаментального шару взаємодії для конкретного інтерфейсу головним завданням варто виключити будь-які "підводні камені" у його функціонуванні і забезпечити хорошу встроєний цього шару як компонента для будь-якого рішення. До речі, методи реалізації такої парадигми називаються патернами, а вміння їх застосовувати є прерогативою грамотних програмістів. Отже, дана стаття і буде показовим прикладом того, як увага до дрібниць створює якісні рішення.
Ознайомлення з ВКонтакте API
Як завжди, ми починаємо знайомство з інтерфейсом з відкриття головної сторінки документації API.





Тут нам дають зрозуміти, що для того, щоб працювати з даними в ВК, потрібно зареєструвати додаток.





Після цього встановлюємо стан на "Включено і видно всім" і відразу копіюємо ID додатка в код.





Робота з авторизацією ВКонтакте API
У першій главі документації ми бачимо, що програма зможе працювати тільки в тому випадку, якщо користувач соціальної мережі справить авторизацію в його контексті й дасть добро на права доступу.





Це можна виконати тільки за допомогою виклику браузера (даний момент обіцяє труднощі).





Обмеження на частоту запитів ВКонтакте API
Будь-інтерфейс, який розрахований на масове використання, зобов'язаний володіти елементарними механізмом обмеження потоку даних для того, щоб йому не могли дати більше запитів, ніж він здатний виконати.
Виклик більшої кількості запитів на момент часу, ніж це дозволяє обмеження, призведе до того, що замість даних клієнт буде отримувати повідомлення про помилки. У зв'язку з цим на самому первинному рівні потрібно забезпечити логіку, згідно з якою всі звернення до API шикуються в чергу і розтягуються в часі. Враховуючи те, що в серйозних програмах звернення виконуються паралельно і асинхронно, реалізація такої архітектури навряд чи буде тривіальною задачею.





Обмеження на обсяги повернутих даних ВКонтакте API
Для абсолютної стабільності роботи інтерфейсу обмежити частоту запитів мало: уявіть, яке доведеться сервера, якщо мережа з декількох тисяч ботів одночасно побажає отримати весь список передплатників спільноти MDK користувачів, що знаходяться в мережі. Щоб уникнути подібних випадків API встановлює пороговий максимум для масивів даних. Це означає, що нам обов'язково потрібно буде реалізувати функціонал, який, спираючись на "чергу" з попереднього абзацу, зможе призначати розподілені виклики множин однотипних методів, відстежувати загальний прогрес їх виконання і повертати результат у вигляді об'єднаних масивів.





Розробка логіки авторизації для ВКонтакте API
Після ознайомлення з інтерфейсом і з'ясування проблемних моментів ми готові приступити безпосередньо до розробки. В першу чергу це буде механізм авторизації, так як при його відсутності переважна більшість функціоналу API просто недоступно.
На самому початку потрібно визначити клас, який буде містити установки для виконання авторизації:
public class VKAPIAuthorizationSettings
{
// > Дозволу, що передаються в параметрі "scope"
public VKAPIAuthorizationPermissions ApplicationPermissions { get; set; }

// > Ідентифікатор додатки ВКонтакте
public String ApplicationIdentity { get; set; }

// > Прапор на постійне переспрашивание авторизації
public Boolean RevocationEnabled { get; set; }

// > Використовувана версія API 
public String APIVersion { get; set; }

// > Створення рядка запиту на основі параметрів
public Uri GetAuthorizationUri()
{
// + Ініціалізація рядка запиту на авторизацію
var authorizationUriBuilder = new UriBuilder("https://oauth.vk.com/authorize");
var queryBuilder = HttpUtility.ParseQueryString(String.Empty);
// ++ Присвоювання неизменяющихся параметрів
queryBuilder["display"] = "popup";
queryBuilder["response_type"] = "token";
queryBuilder["redirect_uri"] = "https://oauth.vk.com/blank.html";
// -- Присвоювання неизменяющихся параметрів
// ++ Присвоювання параметрів, переданих в конфігурації
queryBuilder["v"] = APIVersion;
queryBuilder["client_id"] = ApplicationIdentity;
queryBuilder["scope"] = ApplicationPermissions.ToString();
if (RevocationEnabled) queryBuilder["revoke"] = "1";
// -- Присвоювання параметрів, переданих в конфігурації
authorizationUriBuilder.Query = queryBuilder.ToString();
// - Ініціалізація рядка запиту на авторизацію
return authorizationUriBuilder.Uri;
}
}

У ньому міститься кілька полів і звичайний метод, що повертає URI для авторизації відповідно до того, як це описано в керівництві. До речі, у вкладеному класі дозволів можна помітити цікаві моменти:
public class VKAPIAuthorizationPermissions
{
// > Назви полів в точності копіюють такі в специфікації API
public bool notify;
public bool friends;
public bool photos;
public bool audio;
public bool video;
public bool docs;
public bool notes;
public bool pages;
public bool status;
public bool wall;
public bool groups;
public bool messages;
public bool email;
public bool notifications;
public bool stats;
public bool ads;
public bool market;
public bool offline = true; // > Для отримання неистекаемого ключа (важливо)
public bool nohttps;

// > Метод, що перетворює об'єкт дозволів рядковий фрагмент для URI
public override String ToString()
{
// >> Ця операція - одне з проявів використання "рефлексії"
// >> Всередині класу відбувається читання назв (!) його ж полів
var fieldsInformation = 
typeof(VKAPIAuthorizationPermissions)
.GetFields(BindingFlags.Public | BindingFlags.Instance);

var includedPermissions = new List < String>();

foreach (var fieldInfo in fieldsInformation)
{
// >>> Якщо сканируемое поле має значення true
if ((bool)fieldInfo.GetValue(this))
{
// >>>> Додаємо назва цього поля в список рядків
includedPermissions.Add(fieldInfo.Name);
}
}
// >> По завершенню операції повертаємо назви полів, розділені комами
return String.Join(",", includedPermissions);
}
}

Логіка формування запиту для авторизації готова, тепер можна зайнятися їй самій. Наскільки нам вже відомо, для цього потрібен діалог на WPF з вбудованим браузером з бібліотеки Windows Forms:
Чому стоковий Браузера з WPF не підходить для цього завданняМеханізм авторизації ВКонтакте API влаштований так, щоб повертати посилання, параметри якої відокремлюються від адреси не знаком питання, а ґратами. На свій подив я виявив, що Браузера з WPF не може розпарсити цю конструкцію і повернути дані, а завдяки пошукам на StackOverflow і MSDN з'ясувалося, що такий баг дійсно є і єдиним варіантом є підключити веб-Браузера з бібліотеки Windows Forms.
<x Window:Class="VKAPIUtilities.VKAPIAdapter.Authorization.VKAPIAuthorizationWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:wf="clr-namespace System.Windows.Forms;assembly=System.Windows.Forms"
xmlns:wfi="clr-namespace:System.Windows.Forms.Integration;assembly=WindowsFormsIntegration"
ResizeMode="CanMinimize"
Loaded="Window_Loaded"
SizeToContent="WidthAndHeight"
WindowState="Minimized"
Icon="pack://application:,,,/VKAPIUtilities.VKAPIAdapter;component/Resources/Icons/vk.png">
<!-- Простору імен wf і wfi вимагають включення в проект бібліотек Windows Forms -->
<Grid>
<wfi:WindowsFormsHost>
<wf:WebBrowser
x:Name="браузера"
ScrollBarsEnabled="False" />
</wfi:WindowsFormsHost>
</Grid>
</Window>

Перед тим, як розглянути код діалогу, важливо помітити, що ми чекаємо від нього наступного результату:
public class VKAPIAuthorizationResult
{
// > Ідентифікатор користувача, від імені якого сталася авторизація
public String UserIdentity { get; set; }
// > Ключ, який повертається при успішній авторизації
public String AccessToken { get; set; }
}

Для початку проаналізуємо момент ініціалізації діалогу. Як бачите, його конструктор приймає установки, описані в попередніх абзацах, а коли примірник діалогу на браузер прив'язуються обробники подій і запускається перехід за посиланням авторизації, отриманої з об'єкта установок:
public VKAPIAuthorizationWindow(VKAPIAuthorizationSettings authorizationSettings)
{
_authorizationSettings = authorizationSettings;
}

private void Window_Loaded(object sender, RoutedEventArgs arguments)
{
// > Браузера базується на движку IE і "не розуміє" останніх стандартів JavaScript 
// > Дією в рядку нижче ми вимикаємо повідомлення про кожному невідповідність в скриптах
веб-браузера.ScriptErrorsSuppressed = true;

// > Викликається, коли браузер починає переходити за певною ссылке
веб-браузера.Navigated += WebBrowser_Navigated;

// > Викликається, коли у вікні браузера сторінка завантажилася цілком
веб-браузера.DocumentCompleted += WebBrowser_DocumentCompleted;

// > Інтерфейс готовий, обробники прив'язані - можна починати авторизацію
веб-браузера.Navigate(_authorizationSettings.GetAuthorizationUri());
}

В оброблювачі переходів і є той заповітний код, який оберігає нас від "пострілу собі в ногу":
private void WebBrowser_Navigated(object sender, WebBrowserNavigatedEventArgs arguments)
{
// > Поки сторінка не завантажилася, звернемо вікно
WindowState = WindowState.Minimized; 
// > Дістанемо з URI частина, яка йде після адреси
var uriQueryFragment = arguments.Url.Fragment;
// > При авторизації VK повертає посилання типу https://oauth.vk.com/blank.html#access_token=...
// > Це особлива ситуація, так як відділення параметрів знаком # не є стандартом
if (uriQueryFragment.StartsWith("#"))
{
// >> Для того, щоб парсер зміг обробити фрагмент запиту, потрібно прибрати цей символ
uriQueryFragment = uriQueryFragment.Replace("#", String.Empty);
}
// > Відповідно, тепер можна її розпарсити
var queryParameters = HttpUtility.ParseQueryString(uriQueryFragment);
// > Стан інтерфейсу авторизації потрібно відстежувати за параметрами у рядку навігації
// > У певую чергу перевіримо, чи не містить рядок параметра, який означає скасування
var isCancelledByUser = !String.IsNullOrEmpty(queryParameters["error"]);
if (isCancelledByUser)
{
// >> Якщо такий є, завершимо діалог
DialogResult = false;
}
else
{
// >> Якщо користувач не відміняв процес, можливо, він як раз авторизувався
var isAccessTokenObtained = !String.IsNullOrEmpty(queryParameters["access_token"]);
var isUserIdentityObtained = !String.IsNullOrEmpty(queryParameters["user_id"]);
if (isAccessTokenObtained && isUserIdentityObtained)
{
// >>> У такому випадку запишемо отримані параметри в змінну
_authorizationResult = new VKAPIAuthorizationResult
{
AccessToken = queryParameters["access_token"],
UserIdentity = queryParameters["user_id"]
};
// >>> Тепер з завершенням діалогу можна повернути дані
DialogResult = true;
}
else
{
// >> Поки користувач нічого не скасовував і ще не авторизувався
}
}
}

Не зайвим буде розкрити момент реалізації кастомного діалогу:
new public VKAPIAuthorizationResult ShowDialog()
{
InitializeComponent();
// > Програма зупиняється на цьому місці, поки не присвоєно DialogResult
base.ShowDialog();
// > Коли DialogResult буде присвоєно в коді (або за допомогою закриття вікна)
// > Базовий метод ShowDialog завершиться і цей метод поверне дані
// > Якщо процес скасовано і до заповнення змінної не дійшло, вона буде null
return _authorizationResult;
}

Розробка логіки виконання запитів для ВКонтакте API
Розібравшись з авторизацією і переконавшись, що вона працює так, як очікується, перейдемо до запитів.
В поточному випадку найбільш важливим завданням є обмеження частоти виконання запитів без втрати продуктивності і з збереженням прозорості в частині розподілу потоків. У процесі пошуку інформації з'ясувалося, що на цю тему існує публікація від розробника по імені Jack Leitch, в якій він розглядає кілька варіантів реалізації такої "черзі" і завершує розповідь посиланням на код найоптимальнішого з них.
За традицією, почнемо з опису того, як виглядає структура даних, що представляє сам запит:
public class VKAPIGetRequestDescriptor
{
// > Назва викликаного методу API
public String MethodName { get; set; }

// > Словник параметрів запиту
public Dictionary<String,String> Parameters { get; set; }

// > Формування посилання для запиту, не вимагає авторизації
public Uri GetUnauthorizedRequestUri()
{
var query = String.Join("&", Parameters.Select(item => String.Format("{0}={1}", item.Key, item.Value)));
return new Uri(String.Format("https://api.vk.com/method/{0}?{1}", MethodName, query));
}

// > Формування посилання для запиту, який вимагає авторизації
public Uri GetAuthorizedRequestUri(VKAPIAuthorizationResult authorizationResult)
{
var query = String.Join("&", Parameters.Select(item => String.Format("{0}={1}", item.Key, item.Value)));
return new Uri(
String.Format("https://api.vk.com/method/{0}?{1}&access_token={2}",
MethodName,
query,
authorizationResult.AccessToken));
}
}

Тепер розглянемо логіку виконання одиничного запиту до ВКонтакте API:
// > Обмежувач кількості виконуваних запитів на одиницю часу
private static RateGate _apiRequestsRateGate = new RateGate(2, TimeSpan.FromMilliseconds(1000));

// > Асинхронний метод виконання запиту до API без авторизації
public static void PerformSingleUnauthorizedVKAPIGetrequestasync(
VKAPIGetRequestDescriptor requestDescriptor, // >> Об'єкт запиту
Action<Double> onDownloadProgressChanged, // >> Делегат на передачу прогресу виконання
Action<String> onDownloadCompleted) // >> Делегат на передачу результату виконання
{
// >> Виклик цього методу відкладає виконання контексту, де він викликався, в чергу 
_apiRequestsRateGate.WaitToProceed();

using (var webClient = new WebClient())
{
// >>> Прив'язка обробника на зміну прогресу завантаження
webClient.DownloadProgressChanged += (object sender, DownloadProgressChangedEventArgs arguments) =>
{
// >>>> Викликається делегат, передане в параметрі ззовні
onDownloadProgressChanged(arguments.ProgressPercentage);
};

// >>> Прив'язка обробника на завершення прогресу завантаження
webClient.DownloadStringCompleted += (object sender, DownloadStringCompletedEventArgs arguments) =>
{
// >>>> Викликається делегат, передане в параметрі ззовні
onDownloadCompleted(arguments.Result);
};
// >>> Після прив'язки обробників запускається виконання запиту
webClient.DownloadStringAsync(requestDescriptor.GetUnauthorizedRequestUri());
}
}

Зверніть увагу: ідея вкладених асинхронних методів апріорі передбачає, що завдяки використанню замикань реалізовувати багаторівневі послідовності виконання буде просто, а механізм передачі проміжних параметрів на користувальницький інтерфейс вже фактично готовий.
У свою чергу, логіка пакетного виконання безлічі запитів повторює вищевказану в циклі:
public static void PerformMultipleAuthorizedVKAPIGetrequestsasync(
List<VKAPIGetRequestDescriptor> requestsDescriptors,
VKAPIAuthorizationResult authorizationResult,
Action<Double> onDownloadProgressChanged,
Action<String[]> onDownloadCompleted)
{
// > Кількість переданих об'єктів запитів
Int32 requestsCount = requestsDescriptors.Count();
// > Масив, в якому зберігаються значення прогресу для кожного запиту
Int32[] progressPercentageSegments = new Int32[requestsCount];
// > Змінна, яка зберігає поточне кількість виконаних запитів
Int32 performedRequestsCount = 0;
// > Об'єкт для lock (для запобігання конфлікту потоків за змінну вище)
Object performedRequestsSyncLock = new Object();
// > Масив, в якому зберігаються фрагменти даних від кожного запиту
String[] dataChunks = new String[requestsCount];
// > Циклічний обхід списку переданих запитів
foreach (var request in requestsDescriptors)
{
// >> Регулювання частоти виконання
_apiRequestsRateGate.WaitToProceed();

using (var webClient = new WebClient())
{
webClient.DownloadProgressChanged += (object sender, DownloadProgressChangedEventArgs arguments) =>
{
// >>>> Так як методів було передано багато, відстежуємо загальний (!) відсоток прогресу
progressPercentageSegments[requestsDescriptors.IndexOf(request)] = arguments.ProgressPercentage;
// >>>> При його зміні передаємо на інтерфейс загальне середнє арифметичне
onDownloadProgressChanged(Convert.ToDouble(progressPercentageSegments.Sum()) / requestsCount);
};
webClient.DownloadStringCompleted += (object sender, DownloadStringCompletedEventArgs arguments) =>
{
// >>>> Збереження фгагмента даних в масив рядків
dataChunks[requestsDescriptors.IndexOf(request)] = arguments.Result;
// >>>> Оскільки методи виконуються асинхронно, вони можуть конфліктувати за одну змінну
lock (performedRequestsSyncLock)
{
// >>>>> Щоб цього не сталося, в момент часу доступ буде мати тільки один метод
performedRequestsCount++;
// >>>>> У разі, якщо лічильник виконаних методів дорівнює їх загальної кількості
if (performedRequestsCount == requestsCount)
{
// >>>>>> Виконання безлічі запитів можна вважати завершеним
onDownloadCompleted(dataChunks);
}
}
};
// >>> Запуск одного з безлічі запитів
webClient.DownloadStringAsync(request.GetAuthorizedRequestUri(authorizationResult));
}
}
}

Готовий клієнт для ВКонтакте API .NET
Підводячи підсумки, можна сказати, що дана задача хоч і не простий, але з відповідним підходом для неї з'явилося цілком якісне рішення. Все те, про що я написав у цій статті, укладена в окремій бібліотеці, яку можна вільно завантажити з мого репозиторію на GitHub і використовувати в своїх розробках. Само собою, приклад використання (у вигляді тестової програми) до неї також додається:





Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.