diff --git a/src/Core/Abstractions/IApiService.cs b/src/Core/Abstractions/IApiService.cs index 6184065cf..54ee63451 100644 --- a/src/Core/Abstractions/IApiService.cs +++ b/src/Core/Abstractions/IApiService.cs @@ -47,6 +47,6 @@ namespace Bit.Core.Abstractions Task> GetHibpBreachAsync(string username); Task PostTwoFactorEmailAsync(TwoFactorEmailRequest request); Task PutDeviceTokenAsync(string identifier, DeviceTokenRequest request); - Task PostEventsCollectAsync(EventRequest request); + Task PostEventsCollectAsync(IEnumerable request); } } diff --git a/src/Core/Abstractions/IEventService.cs b/src/Core/Abstractions/IEventService.cs new file mode 100644 index 000000000..0f0a51bf0 --- /dev/null +++ b/src/Core/Abstractions/IEventService.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; +using Bit.Core.Enums; + +namespace Bit.Core.Abstractions +{ + public interface IEventService + { + Task ClearEventsAsync(); + Task CollectAsync(EventType eventType, string cipherId = null, bool uploadImmediately = false); + Task UploadEventsAsync(); + } +} \ No newline at end of file diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index cf4339f70..f9d4f8e0a 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -32,6 +32,7 @@ public static string MigratedFromV1 = "migratedFromV1"; public static string MigratedFromV1AutofillPromptShown = "migratedV1AutofillPromptShown"; public static string TriedV1Resync = "triedV1Resync"; + public static string EventCollectionKey = "eventCollection"; public const int SelectFileRequestCode = 42; public const int SelectFilePermissionRequestCode = 43; } diff --git a/src/Core/Models/Data/EventData.cs b/src/Core/Models/Data/EventData.cs new file mode 100644 index 000000000..49f0957f3 --- /dev/null +++ b/src/Core/Models/Data/EventData.cs @@ -0,0 +1,12 @@ +using Bit.Core.Enums; +using System; + +namespace Bit.Core.Models.Data +{ + public class EventData : Data + { + public EventType Type { get; set; } + public string CipherId { get; set; } + public DateTime Date { get; set; } + } +} diff --git a/src/Core/Models/Request/EventRequest.cs b/src/Core/Models/Request/EventRequest.cs index 817491c68..d1c728287 100644 --- a/src/Core/Models/Request/EventRequest.cs +++ b/src/Core/Models/Request/EventRequest.cs @@ -1,5 +1,5 @@ using Bit.Core.Enums; -using Bit.Core.Models.Domain; +using System; namespace Bit.Core.Models.Request { @@ -7,5 +7,6 @@ namespace Bit.Core.Models.Request { public EventType Type { get; set; } public string CipherId { get; set; } + public DateTime Date { get; set; } } } diff --git a/src/Core/Services/ApiService.cs b/src/Core/Services/ApiService.cs index 68ae77ed8..9280b314e 100644 --- a/src/Core/Services/ApiService.cs +++ b/src/Core/Services/ApiService.cs @@ -298,7 +298,7 @@ namespace Bit.Core.Services #region Event APIs - public async Task PostEventsCollectAsync(EventRequest request) + public async Task PostEventsCollectAsync(IEnumerable request) { using(var requestMessage = new HttpRequestMessage()) { diff --git a/src/Core/Services/EventService.cs b/src/Core/Services/EventService.cs new file mode 100644 index 000000000..44b0011d1 --- /dev/null +++ b/src/Core/Services/EventService.cs @@ -0,0 +1,106 @@ +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Models.Data; +using Bit.Core.Models.Request; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Bit.Core.Services +{ + public class EventService : IEventService + { + private readonly IStorageService _storageService; + private readonly IApiService _apiService; + private readonly IUserService _userService; + private readonly ICipherService _cipherService; + + public EventService( + IStorageService storageService, + IApiService apiService, + IUserService userService, + ICipherService cipherService) + { + _storageService = storageService; + _apiService = apiService; + _userService = userService; + _cipherService = cipherService; + } + + public async Task CollectAsync(EventType eventType, string cipherId = null, bool uploadImmediately = false) + { + var authed = await _userService.IsAuthenticatedAsync(); + if(!authed) + { + return; + } + var organizations = await _userService.GetAllOrganizationAsync(); + if(organizations == null) + { + return; + } + var orgIds = new HashSet(organizations.Where(o => o.UseEvents).Select(o => o.Id)); + if(!orgIds.Any()) + { + return; + } + if(cipherId != null) + { + var cipher = await _cipherService.GetAsync(cipherId); + if(cipher?.OrganizationId == null || !orgIds.Contains(cipher.OrganizationId)) + { + return; + } + } + var eventCollection = await _storageService.GetAsync>(Constants.EventCollectionKey); + if(eventCollection == null) + { + eventCollection = new List(); + } + eventCollection.Add(new EventData + { + Type = eventType, + CipherId = cipherId, + Date = DateTime.UtcNow + }); + await _storageService.SaveAsync(Constants.EventCollectionKey, eventCollection); + if(uploadImmediately) + { + await UploadEventsAsync(); + } + } + + public async Task UploadEventsAsync() + { + var authed = await _userService.IsAuthenticatedAsync(); + if(!authed) + { + return; + } + var eventCollection = await _storageService.GetAsync>(Constants.EventCollectionKey); + if(eventCollection == null || !eventCollection.Any()) + { + return; + } + var request = eventCollection.Select(e => new EventRequest + { + Type = e.Type, + CipherId = e.CipherId, + Date = e.Date + }); + try + { + await _apiService.PostEventsCollectAsync(request); + await ClearEventsAsync(); + } + catch(ApiException) { } + } + + public async Task ClearEventsAsync() + { + await _storageService.RemoveAsync(Constants.EventCollectionKey); + } + } +}