Skip to content

Instantly share code, notes, and snippets.

@Rast1234
Last active January 6, 2019 19:08
Show Gist options
  • Save Rast1234/acfe8476114d4520018156659da04b03 to your computer and use it in GitHub Desktop.
Save Rast1234/acfe8476114d4520018156659da04b03 to your computer and use it in GitHub Desktop.
vkApi tweaks: User-Agent, custom rate limit, CancellationToken
var cts = new CancellationTokenSource();
var sc = new ServiceCollection();
// копипастнутый клиент со своим UserAgent
sc.AddSingleton<IRestClient, RestClientWithUserAgent>();
// копипастнутый ограничитель запросов, можно обойтись штатным CountByIntervalAwaitableConstraint, просто он не работает с внешним CancellationToken.
// тут немного увеличиваем ограничение: 3 запроса за 1.3 секунды
sc.AddSingleton<IAwaitableConstraint, CancellableConstraint>(serviceProvider => new CancellableConstraint(3, TimeSpan.FromSeconds(1.3), cts.Token));
var vkApi = new VkApi(sc);
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using VkNet.Abstractions.Core;
using VkNet.Utils;
namespace VOffline.Services.Vk
{
public class CancellableConstraint : IAwaitableConstraint
{
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
private readonly object _sync = new object();
private readonly LimitedSizeStack<DateTime> _timeStamps;
private int _count;
private TimeSpan _timeSpan;
private readonly CancellationToken token;
public CancellableConstraint(int number, TimeSpan timeSpan, CancellationToken token)
{
if (number <= 0)
throw new ArgumentException("count should be strictly positive", nameof(number));
if (timeSpan.TotalMilliseconds <= 0.0)
throw new ArgumentException("timeSpan should be strictly positive", nameof(timeSpan));
this._count = number;
this._timeSpan = timeSpan;
this.token = token;
this._timeStamps = new LimitedSizeStack<DateTime>(this._count);
}
public async Task<IDisposable> WaitForReadiness(CancellationToken cancellationToken)
{
// чтобы можно было отменить очередь ожидающих запросов, надо иметь токен.
// vknet сам ииспользует этот метод с CancellationToken.None и заменить никак нельзя
// поэтому вот такой обходной путь
using (var cts = CancellationTokenSource.CreateLinkedTokenSource(token, cancellationToken))
{
CancellableConstraint awaitableConstraint = this;
await awaitableConstraint._semaphore.WaitAsync(cts.Token);
int num = 0;
DateTime now = DateTime.Now;
DateTime dateTime = now - awaitableConstraint._timeSpan;
LinkedListNode<DateTime> linkedListNode1 = awaitableConstraint._timeStamps.First;
LinkedListNode<DateTime> linkedListNode2 = (LinkedListNode<DateTime>) null;
while (linkedListNode1 != null && linkedListNode1.Value > dateTime)
{
linkedListNode2 = linkedListNode1;
linkedListNode1 = linkedListNode1.Next;
++num;
}
if (num < awaitableConstraint._count)
return (IDisposable) new DisposableAction(new Action(awaitableConstraint.OnEnded));
TimeSpan delay = linkedListNode2.Value.Add(awaitableConstraint._timeSpan) - now;
try
{
await Task.Delay(delay, cts.Token);
}
catch (Exception ex)
{
awaitableConstraint._semaphore.Release();
throw;
}
return (IDisposable) new DisposableAction(new Action(awaitableConstraint.OnEnded));
}
}
/// <inheritdoc />
public void SetRate(int number, TimeSpan timeSpan)
{
lock (this._sync)
{
this._count = number;
this._timeSpan = timeSpan;
}
}
private void OnEnded()
{
this._timeStamps.Push(DateTime.Now);
this._semaphore.Release();
}
}
// пришлось этот класс вытащить из библиотеки т.к. он не был публичным
public class LimitedSizeStack<T> : LinkedList<T>
{
private readonly int _maxSize;
public LimitedSizeStack(int maxSize)
{
this._maxSize = maxSize;
}
public void Push(T item)
{
this.AddFirst(item);
if (this.Count <= this._maxSize)
return;
this.RemoveLast();
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using VkNet.Abstractions.Utils;
using VkNet.Utils;
namespace Foo
{
public class RestClientWithUserAgent : IRestClient
{
/// <summary>The log</summary>
private readonly ILogger<RestClient> _logger;
private TimeSpan _timeoutSeconds;
/// <inheritdoc />
public RestClientWithUserAgent(ILogger<RestClient> logger, IWebProxy proxy)
{
_logger = logger;
Proxy = proxy;
}
/// <inheritdoc />
public IWebProxy Proxy { get; set; }
/// <inheritdoc />
public TimeSpan Timeout
{
get
{
if (!(this._timeoutSeconds == TimeSpan.Zero))
return this._timeoutSeconds;
return TimeSpan.FromSeconds(300.0);
}
set
{
this._timeoutSeconds = value;
}
}
public Task<HttpResponse<string>> GetAsync(Uri uri,IEnumerable<KeyValuePair<string, string>> parameters)
{
IEnumerable<string> values = parameters.Where<KeyValuePair<string, string>>((Func<KeyValuePair<string, string>, bool>)(parameter => !string.IsNullOrWhiteSpace(parameter.Value))).Select<KeyValuePair<string, string>, string>((Func<KeyValuePair<string, string>, string>)(parameter => parameter.Key.ToLowerInvariant() + "=" + parameter.Value));
UriBuilder uriBuilder = new UriBuilder(uri)
{
Query = string.Join("&", values)
};
ILogger<RestClient> logger = this._logger;
if (logger != null)
logger.LogDebug(string.Format("GET request: {0}", (object)uriBuilder.Uri));
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, uri);
return this.Call((Func<HttpClient, Task<HttpResponseMessage>>)(httpClient => httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)));
}
public Task<HttpResponse<string>> PostAsync(Uri uri,IEnumerable<KeyValuePair<string, string>> parameters)
{
if (this._logger != null)
{
string json = JsonConvert.SerializeObject((object)parameters);
this._logger.LogDebug(string.Format("POST request: {0}{1}{2}", (object)uri, (object)Environment.NewLine, (object)Utilities.PreetyPrintJson(json)));
}
FormUrlEncodedContent urlEncodedContent = new FormUrlEncodedContent(parameters);
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, uri)
{
Content = (HttpContent)urlEncodedContent
};
return this.Call((Func<HttpClient, Task<HttpResponseMessage>>)(httpClient => httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)));
}
private async Task<HttpResponse<string>> Call(Func<HttpClient, Task<HttpResponseMessage>> method)
{
var httpClientHandler = new HttpClientHandler
{
UseProxy = false
};
if (Proxy != null)
{
httpClientHandler = new HttpClientHandler
{
Proxy = Proxy,
UseProxy = true
};
var logger = _logger;
logger?.LogDebug($"Use Proxy: {(object) Proxy}");
}
using (var client = new HttpClient(httpClientHandler))
{
// единственное отличие:
client.DefaultRequestHeaders.UserAgent.ParseAdd("USER AGENT!");
if (Timeout != TimeSpan.Zero)
client.Timeout = Timeout;
var response = await method(client).ConfigureAwait(false);
var requestUri = response.RequestMessage.RequestUri.ToString();
if (!response.IsSuccessStatusCode)
return HttpResponse<string>.Fail(response.StatusCode,
await response.Content.ReadAsStringAsync().ConfigureAwait(false), requestUri);
var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
var logger = _logger;
logger?.LogDebug("Response:" + Environment.NewLine + Utilities.PreetyPrintJson(json));
return HttpResponse<string>.Success(response.StatusCode, json, requestUri);
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment