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

в 13:57, , рубрики: .net, C#, vk api, wpf, Вконтакте, Вконтакте API

Здравствуйте, дорогие читатели!

Мало для кого является секретом, что за последние несколько лет одноимённая социальная сеть успела основательно войти людям в привычку и вырасти до масштабов сервиса континентального уровня.

За это время пространство ВКонтакте активно осваивали все, кто увидел там какой-либо потенциал, и сегодня в нём существует множество проектов, нацеленных на аудиторию с различными предпочтениями.

Когда работа с информацией приобретает решающее значение, становится очевидно, что в стремлении к лучшим результатам люди будут нуждаться в новых и не всегда стандартных решениях обработки данных.

Меня зовут Илья Терещук, на сегодняшний день я живу ведением проекта в социальной сети и занимаюсь программированием. Создавая своё первое приложение для работы с API, я столкнулся с немалым количеством нюансов, которые, по всей видимости, дают изрядно поломать голову любому, кто берётся за подобное впервые.

В разработке фундаментального слоя взаимодействия для конкретного интерфейса главной задачей стоит исключить любые "подводные камни" в его функционировании и обеспечить хорошую встраиваемость этого слоя как компонента для любого решения. К слову, методы реализации такой парадигмы называются паттернами, а умение их применять является прерогативой грамотных программистов. Следовательно, данная статья и будет показательным примером того, как внимание к мелочам создаёт качественные решения.

Ознакомление с ВКонтакте API

Как обычно, мы начинаем знакомство с интерфейсом с открытия главной страницы документации API.


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


Здесь нам дают понять, что для того, чтобы работать с данными в ВК, нужно зарегистрировать приложение.


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


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


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


Работа с авторизацией ВКонтакте API

В первой главе документации мы видим, что приложение сможет работать только в том случае, если пользователь социальной сети произведёт авторизацию в его контексте и даст добро на права доступа.


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


Это можно выполнить только посредством вызова браузера (данный момент обещает трудности).


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


Ограничение на частоту запросов ВКонтакте API

Любой интерфейс, который рассчитан на массовое использование, обязан обладать элементарным механизмом ограничения потока данных для того, чтобы ему не могли дать больше запросов, чем он способен выполнить.
Вызов большего количества запросов на момент времени, чем это позволяет ограничение, приведёт к тому, что вместо данных клиент будет получать сообщения об ошибках. В связи с этим на самом первичном уровне нужно обеспечить логику, согласно которой все обращения к API выстраиваются в очередь и растягиваются по времени. Учитывая то, что в серьёзных приложениях обращения выполняются параллельно и асинхронно, реализация такой архитектуры вряд ли будет тривиальной задачей.


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


Ограничение на объёмы возвращаемых данных ВКонтакте API

Для абсолютной стабильности работы интерфейса ограничить частоту запросов мало: представьте, каково придётся серверу, если сеть из нескольких тысяч ботов одновременно пожелает получить весь список подписчиков сообщества MDK пользователей, находящихся в сети. Во избежание подобных случаев API устанавливает пороговый максимум для массивов данных. Это означает, что нам обязательно нужно будет реализовать функционал, который, опираясь на "очередь" из предыдущего абзаца, сможет назначать распределённые вызовы множеств однотипных методов, отслеживать общий прогресс их выполнения и возвращать результат в виде объединённых массивов.


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


Разработка логики авторизации для ВКонтакте 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:

Почему стоковый WebBrowser из WPF не подходит для этой задачи

Механизм авторизации ВКонтакте API устроен так, чтобы возвращать ссылку, параметры в которой отделяются от адреса не знаком вопроса, а решёткой. К своему удивлению я обнаружил, что WebBrowser из WPF не может распарсить эту конструкцию и вернуть данные, а благодаря поискам на StackOverflow и MSDN выяснилось, что такой баг действительно есть и единственным вариантом является подключить WebBrowser из библиотеки Windows Forms.

<Window x: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="webBrowser"
                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)
{
    // > WebBrowser базируется на движке IE и "не понимает" последних стандартов JavaScript 
    // > Действием в строке ниже мы выключаем сообщения о каждом несоответствии в скриптах
    webBrowser.ScriptErrorsSuppressed = true;

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

    // > Вызывается, когда в окне браузера страница загрузилась целиком
    webBrowser.DocumentCompleted += WebBrowser_DocumentCompleted;

    // > Интерфейс готов, обработчики привязаны - можно начинать авторизацию
    webBrowser.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
        {
            // >> Пока пользователь ничего не отменял и еще не авторизовался
        }
    }
}

Не лишним будет раскрыть момент реализации кастомного диалога:

public new 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 и использовать в своих разработках. Само собой, пример использования (в виде тестовой программы) к ней тоже прилагается:


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


Автор: IlyaTereschuk

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js