From 70fa469f29425a240213570d97b46edcc472cdbe Mon Sep 17 00:00:00 2001 From: Ivan Melentyev Date: Mon, 11 Sep 2023 21:56:58 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D1=8F=20=D0=BF=D1=80=D0=BE=D1=81=D1=82=D0=BE=D0=B3?= =?UTF-8?q?=D0=BE=20=D0=BB=D0=BE=D0=BD=D0=B3=D0=BF=D1=83=D0=BB=D0=B0=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D1=82=D0=B5=D0=BB=D0=B5=D0=B9.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- VkNet/Model/LongPollHistoryResponse.cs | 11 +- .../Utils/BotsLongPool/BotsLongPoolHelpers.cs | 2 +- .../BotsLongPoolUpdatesHandlerParams.cs | 4 +- .../IUsersLongPoolUpdatesHandler.cs | 17 ++ VkNet/Utils/UsersLongPool/UserMessageEvent.cs | 26 ++ .../UsersLongPool/UsersLongPoolHelpers.cs | 48 ++++ .../UsersLongPoolOnUpdatesEvent.cs | 22 ++ .../UsersLongPoolUpdatesHandler.cs | 225 ++++++++++++++++++ .../UsersLongPoolUpdatesHandlerParams.cs | 73 ++++++ VkNet/Utils/VkErrors.cs | 48 ++++ 10 files changed, 471 insertions(+), 5 deletions(-) create mode 100644 VkNet/Utils/UsersLongPool/IUsersLongPoolUpdatesHandler.cs create mode 100644 VkNet/Utils/UsersLongPool/UserMessageEvent.cs create mode 100644 VkNet/Utils/UsersLongPool/UsersLongPoolHelpers.cs create mode 100644 VkNet/Utils/UsersLongPool/UsersLongPoolOnUpdatesEvent.cs create mode 100644 VkNet/Utils/UsersLongPool/UsersLongPoolUpdatesHandler.cs create mode 100644 VkNet/Utils/UsersLongPool/UsersLongPoolUpdatesHandlerParams.cs diff --git a/VkNet/Model/LongPollHistoryResponse.cs b/VkNet/Model/LongPollHistoryResponse.cs index 0596df95c..d346c102e 100644 --- a/VkNet/Model/LongPollHistoryResponse.cs +++ b/VkNet/Model/LongPollHistoryResponse.cs @@ -14,7 +14,7 @@ namespace VkNet.Model; /// Обновления в личных сообщениях пользователя. /// [Serializable] -public class LongPollHistoryResponse +public class LongPollHistoryResponse { /// /// Обновления в личных сообщениях пользователя. @@ -25,6 +25,7 @@ public class LongPollHistoryResponse /// История. /// [JsonProperty("history")] + // ReSharper disable once AutoPropertyCanBeMadeGetOnly.Global public List> History { get; set; } @@ -37,7 +38,7 @@ public class LongPollHistoryResponse /// Колекция сообщений. /// [JsonProperty("messages")] - public VkCollection Messages { get; set; } + public VkCollection Messages { get; set; } /// /// Колекция профилей. @@ -65,4 +66,10 @@ public class LongPollHistoryResponse /// [JsonProperty("more")] public bool More { get; set; } +} + +/// +[Serializable] +public class LongPollHistoryResponse : LongPollHistoryResponse +{ } \ No newline at end of file diff --git a/VkNet/Utils/BotsLongPool/BotsLongPoolHelpers.cs b/VkNet/Utils/BotsLongPool/BotsLongPoolHelpers.cs index 1820d7717..322dcd254 100644 --- a/VkNet/Utils/BotsLongPool/BotsLongPoolHelpers.cs +++ b/VkNet/Utils/BotsLongPool/BotsLongPoolHelpers.cs @@ -17,7 +17,7 @@ public static class BotsLongPoolHelpers /// /// Возвращает список обновлений группы /// - public static List GetGroupUpdateEvents(List jObjectUpdates) + public static List GetGroupUpdateEvents(IEnumerable jObjectUpdates) { var updates = new List(); diff --git a/VkNet/Utils/BotsLongPool/BotsLongPoolUpdatesHandlerParams.cs b/VkNet/Utils/BotsLongPool/BotsLongPoolUpdatesHandlerParams.cs index 1fb708b17..85b47c9aa 100644 --- a/VkNet/Utils/BotsLongPool/BotsLongPoolUpdatesHandlerParams.cs +++ b/VkNet/Utils/BotsLongPool/BotsLongPoolUpdatesHandlerParams.cs @@ -6,7 +6,7 @@ namespace VkNet.Utils.BotsLongPool; /// -/// Параметры для конструктора BotsLongPoolUpdatesProvider +/// Параметры для конструктора BotsLongPoolUpdatesHandler /// [UsedImplicitly] public class BotsLongPoolUpdatesHandlerParams @@ -58,7 +58,7 @@ public BotsLongPoolUpdatesHandlerParams(IVkApi api, ulong groupId) public Action? OnUpdates { get; set; } = null; /// - /// Функция, в которую будет отправляться ts при каждом его обновлении. + /// Функция, в которую будет отправляться TS при каждом его обновлении. /// public Action? OnTsChange { get; set; } = null; diff --git a/VkNet/Utils/UsersLongPool/IUsersLongPoolUpdatesHandler.cs b/VkNet/Utils/UsersLongPool/IUsersLongPoolUpdatesHandler.cs new file mode 100644 index 000000000..74278f54f --- /dev/null +++ b/VkNet/Utils/UsersLongPool/IUsersLongPoolUpdatesHandler.cs @@ -0,0 +1,17 @@ +#nullable enable +using System.Threading; +using System.Threading.Tasks; + +namespace VkNet.Utils.UsersLongPool; + +/// +/// Обработчик лонгпула пользовательских сообщений +/// +public interface IUsersLongPoolUpdatesHandler +{ + /// + /// Запуск отслеживания событий + /// + /// Токен отмены операции + Task RunAsync(CancellationToken token = default); +} \ No newline at end of file diff --git a/VkNet/Utils/UsersLongPool/UserMessageEvent.cs b/VkNet/Utils/UsersLongPool/UserMessageEvent.cs new file mode 100644 index 000000000..68ead8201 --- /dev/null +++ b/VkNet/Utils/UsersLongPool/UserMessageEvent.cs @@ -0,0 +1,26 @@ +#nullable enable +using Newtonsoft.Json.Linq; +using VkNet.Model; + +namespace VkNet.Utils.UsersLongPool; + +/// +/// Обёртка для Message, в которой кроме самого сообщения есть и ошибки при парсинге. +/// +public class UserMessageEvent +{ + /// + /// Сообщение + /// + public Message? Message = null; + + /// + /// Ошибка парсинга сообщения + /// + public System.Exception? Exception = null; + + /// + /// Сообщение в JObject + /// + public JObject RawMessage; +} \ No newline at end of file diff --git a/VkNet/Utils/UsersLongPool/UsersLongPoolHelpers.cs b/VkNet/Utils/UsersLongPool/UsersLongPoolHelpers.cs new file mode 100644 index 000000000..3283e06df --- /dev/null +++ b/VkNet/Utils/UsersLongPool/UsersLongPoolHelpers.cs @@ -0,0 +1,48 @@ +#nullable enable +using System.Collections.Generic; +using Newtonsoft.Json.Linq; +using VkNet.Model; + +namespace VkNet.Utils.UsersLongPool; + +/// +/// Методы для обработки событий лонгпула у сообществ. +/// +public static class UsersLongPoolHelpers +{ + /// + /// Метод для получения сообщений из массива JObject, который не бросает исключений, но вместе с сообщениями возвращает ошибки при десериализации, если таковые имеются. + /// + /// Этот массив получается из метода api.Messages.GetLongPollHistory<LongPollHistoryResponse<JObject>>().Messages + /// + /// Возвращает список сообщений пользователя + /// + public static List GetUserMessageEvents(IEnumerable jObjectMessages) + { + var userMessageEvents = new List(); + + foreach (var jObjectMessage in jObjectMessages) + { + try + { + var message = jObjectMessage.ToObject(); + + userMessageEvents.Add(new() + { + Message = message, + RawMessage = jObjectMessage + }); + } + catch (System.Exception ex) + { + userMessageEvents.Add(new() + { + Exception = ex, + RawMessage = jObjectMessage + }); + } + } + + return userMessageEvents; + } +} \ No newline at end of file diff --git a/VkNet/Utils/UsersLongPool/UsersLongPoolOnUpdatesEvent.cs b/VkNet/Utils/UsersLongPool/UsersLongPoolOnUpdatesEvent.cs new file mode 100644 index 000000000..90d436cb4 --- /dev/null +++ b/VkNet/Utils/UsersLongPool/UsersLongPoolOnUpdatesEvent.cs @@ -0,0 +1,22 @@ +#nullable enable +using System.Collections.Generic; +using Newtonsoft.Json.Linq; +using VkNet.Model; + +namespace VkNet.Utils.UsersLongPool; + +/// +/// Обёртка для UsersLongPoolUpdatesHandlerParams.OnUpdates, в которой содержится вся информация о текущем массиве событий лонгпула для пользователя. +/// +public class UsersLongPoolOnUpdatesEvent +{ + /// + /// Обновление в событиях пользователя. + /// + public LongPollHistoryResponse Response; + + /// + /// Обработанные сообщения из Response + /// + public List Messages; +} \ No newline at end of file diff --git a/VkNet/Utils/UsersLongPool/UsersLongPoolUpdatesHandler.cs b/VkNet/Utils/UsersLongPool/UsersLongPoolUpdatesHandler.cs new file mode 100644 index 000000000..8d36d31fa --- /dev/null +++ b/VkNet/Utils/UsersLongPool/UsersLongPoolUpdatesHandler.cs @@ -0,0 +1,225 @@ +#nullable enable +using System; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using VkNet.Exception; +using VkNet.Model; + +namespace VkNet.Utils.UsersLongPool; + +/// +/// Реализация лонгпула для пользователя +/// +[UsedImplicitly] +public class UsersLongPoolUpdatesHandler : IUsersLongPoolUpdatesHandler +{ + private readonly UsersLongPoolUpdatesHandlerParams _params; + + private ulong? _currentTs; + + private ulong? _currentPts; + + /// + /// Инициализирует новый экземпляр класса + /// + public UsersLongPoolUpdatesHandler(UsersLongPoolUpdatesHandlerParams @params) => _params = @params; + + /// + /// Запуск отслеживания событий + /// + /// Токен отмены операции + [UsedImplicitly] + public async Task RunAsync(CancellationToken token = default) + { + while (!token.IsCancellationRequested) + { + while (_params.GetPause?.Invoke() is not true) + { + token.ThrowIfCancellationRequested(); + await NextLongPoolHistoryAsync(token: token); + } + + await Task.Delay(_params.DelayBetweenUpdates, token); + } + } + + private async Task NextLongPoolHistoryAsync(CancellationToken token = default) + { + try + { + if (_currentPts is null || _currentTs is null) + { + await InitCurrentTsAndPtsAsync(token); + + VkErrors.ThrowIfUlongIsNull(() => _currentTs); + } + + var response = await _params.Api.Messages.GetLongPollHistoryAsync>(new() + { + Pts = _currentPts, + Ts = _currentTs!.Value, + }, token); + + // если сообщений нет - игнорируем + if (!response.Messages.Any()) + { + return; + } + + SetPts(response.NewPts); + var userMessageEvents = UsersLongPoolHelpers.GetUserMessageEvents(response.Messages); + + _params.OnUpdates?.Invoke(new() + { + Response = response, + Messages = userMessageEvents + }); + } + catch (System.Exception ex) + { + await HandleExceptionAsync(ex, token); + } + } + + private async Task HandleExceptionAsync(System.Exception exception, CancellationToken token) + { + switch (exception) + { + case LongPollException longPollException: + await HandleLongPoolExceptionAsync(longPollException, token); + + return; + + case JsonReaderException or JsonSerializationException: + _params.OnException?.Invoke(exception); + IncPts(); + + return; + + case HttpRequestException or PublicServerErrorException: + _params.OnWarn?.Invoke(exception); + + return; + + case SocketException or TaskCanceledException or IOException or WebException: + return; + + default: + _params.OnException?.Invoke(exception); + + break; + } + } + + /// + /// Обработать ошибку, связанную с лонгпулом + /// + /// Ошибка, связанная с лонгпулом + /// Токен отмены операции + private async Task HandleLongPoolExceptionAsync(LongPollException exception, CancellationToken token) + { + switch (exception) + { + case LongPollOutdateException outdatedException: + if (_currentTs is null) + { + throw new($"{nameof(_currentTs)} is null"); + } + + SetTs(outdatedException.Ts); + + break; + + default: + try + { + await UpdateLongPoolServerAsync(token); + } + catch (System.Exception ex) + { + await HandleExceptionAsync(ex, token); + } + + break; + } + } + + private async Task InitCurrentTsAndPtsAsync(CancellationToken token) + { + try + { + var response = await _params.Api.Messages.GetLongPollServerAsync(groupId: _params.GroupId, token: token); + SetTs(response.Ts); + var pts = _params.Pts ?? response.Pts; + + if (pts is not null) + { + SetPts(pts.Value); + } + } + catch (System.Exception ex) + { + await HandleExceptionAsync(ex, token); + } + } + + private async Task UpdateLongPoolServerAsync(CancellationToken token) + { + try + { + var response = await _params.Api.Messages.GetLongPollServerAsync(groupId: _params.GroupId, token: token); + _currentTs = response.Ts; + _currentPts = response.Pts; + + if (_currentTs is null) + { + SetTs(response.Ts); + } + + if (_currentPts is null && response.Pts is not null) + { + SetPts(response.Pts.Value); + } + } + catch (System.Exception ex) + { + await HandleExceptionAsync(ex, token); + } + } + + private void SetTs(ulong ts) + { + _currentTs = ts; + _params.OnTsChange?.Invoke(_currentTs.Value); + } + + private void SetPts(ulong pts) + { + _currentPts = pts; + _params.OnPtsChange?.Invoke(_currentPts.Value); + } + + /// + /// Увеличить текущий номер события на 1. Это нужно в том случае, если ВК чудит, например - присылает старый номер PTS. + /// А так же это пригодится, если вылезет ошибка JsonSerializationException. + /// В таком случае мы не сможем получить новый PTS и только прибавив 1 мы сможем получить следующий массив обновлений без текущей ошибки. + /// + /// Этот метод может быть вызван только после вызова InitCurrentTsAndPtsAsync + private void IncPts() + { + if (_currentPts is null) + { + throw new($"{nameof(_currentPts)} is null"); + } + + SetPts(_currentPts.Value + 1); + } +} \ No newline at end of file diff --git a/VkNet/Utils/UsersLongPool/UsersLongPoolUpdatesHandlerParams.cs b/VkNet/Utils/UsersLongPool/UsersLongPoolUpdatesHandlerParams.cs new file mode 100644 index 000000000..614c1bd0c --- /dev/null +++ b/VkNet/Utils/UsersLongPool/UsersLongPoolUpdatesHandlerParams.cs @@ -0,0 +1,73 @@ +#nullable enable +using System; +using JetBrains.Annotations; +using VkNet.Abstractions; + +namespace VkNet.Utils.UsersLongPool; + +/// +/// Параметры для конструктора UsersLongPoolUpdatesHandler +/// +[UsedImplicitly] +public class UsersLongPoolUpdatesHandlerParams +{ + /// + /// Айди группы, от которой получать данные + /// + public ulong? GroupId { get; set; } + + /// + /// Номер, с которого начинать получать события. + /// Рекомендуется помещать сюда значение из response.Pts в функции OnUpdates. + /// + public ulong? Pts { get; set; } = null; + + /// + /// Авторизованный экземпляр VKApi. + /// + public IVkApi Api { get; set; } + + /// + /// Инициализирует новый экземпляр класса + /// + public UsersLongPoolUpdatesHandlerParams(IVkApi api) + { + Api = api; + } + + /// + /// Ожидание между обработкой событий при простое + /// + public TimeSpan DelayBetweenUpdates { get; set; } = TimeSpan.FromSeconds(1); + + /// + /// Функция, которая возвращает true, если работа лонгпула должна быть приостановлена + /// Понадобится, когда вы безопасно завершаете работу приложения или просто захотите временно остановить бота и не потерять последние события. + /// + public Func? GetPause { get; set; } = null; + + /// + /// Функция, в которую будут отправлены полученные события. + /// + public Action? OnUpdates { get; set; } = null; + + /// + /// Функция, в которую будет отправляться TS при каждом его обновлении. + /// + public Action? OnTsChange { get; set; } = null; + + /// + /// Функция, в которую будет отправляться PTS при каждом его обновлении. + /// + public Action? OnPtsChange { get; set; } = null; + + /// + /// Эта функция вызывается при критических ошибках в лонгпуле (например JsonSerializationException) + /// + public Action? OnException { get; set; } = null; + + /// + /// Функция, в которую будут отправлены незначительные или временные ошибки (например - SocketException или ошибки связанные с интернетом или с доступом к ВКонтакте) + /// + public Action? OnWarn { get; set; } = null; +} \ No newline at end of file diff --git a/VkNet/Utils/VkErrors.cs b/VkNet/Utils/VkErrors.cs index f8fca842f..3263184de 100644 --- a/VkNet/Utils/VkErrors.cs +++ b/VkNet/Utils/VkErrors.cs @@ -37,6 +37,54 @@ public static void ThrowIfNullOrEmpty(Expression> expr) } } + /// + /// Ошибка если число равно null. + /// + /// Выражение. + /// + /// Параметр не должен быть равен + /// null + /// + public static void ThrowIfLongIsNull(Expression> expr) + { + if (expr.Body is not MemberExpression body) + { + return; + } + + var paramName = body.Member.Name; + var value = expr.Compile()(); + + if (value is null) + { + throw new ArgumentNullException(paramName, "Параметр не должен быть равен null"); + } + } + + /// + /// Ошибка если число равно null. + /// + /// Выражение. + /// + /// Параметр не должен быть равен + /// null + /// + public static void ThrowIfUlongIsNull(Expression> expr) + { + if (expr.Body is not MemberExpression body) + { + return; + } + + var paramName = body.Member.Name; + var value = expr.Compile()(); + + if (value is null) + { + throw new ArgumentNullException(paramName, "Параметр не должен быть равен null"); + } + } + /// /// Ошибка если число не в диапазоне. ///