diff --git a/Manager.App/Components/App.razor b/Manager.App/Components/App.razor index 68d8c96..fe48475 100644 --- a/Manager.App/Components/App.razor +++ b/Manager.App/Components/App.razor @@ -2,7 +2,7 @@ - Application + YouTube Manager server @@ -11,7 +11,6 @@ - diff --git a/Manager.App/Components/Application/System/EventConsole.razor.cs b/Manager.App/Components/Application/System/EventConsole.razor.cs index dfd01d8..1a85c9d 100644 --- a/Manager.App/Components/Application/System/EventConsole.razor.cs +++ b/Manager.App/Components/Application/System/EventConsole.razor.cs @@ -107,7 +107,11 @@ public partial class EventConsole : ComponentBase _batchLock.Release(); } - _serviceEvents.AddRange(batch); + foreach (var serviceEvent in _batchBuffer.Where(serviceEvent => !_serviceEvents.Contains(serviceEvent))) + { + _serviceEvents.Add(serviceEvent); + } + _lastBatchUpdate = DateTime.UtcNow; if (_virtualize != null) diff --git a/Manager.App/Components/Pages/Channels.razor b/Manager.App/Components/Pages/Channels.razor index 80bbd3e..18010dd 100644 --- a/Manager.App/Components/Pages/Channels.razor +++ b/Manager.App/Components/Pages/Channels.razor @@ -6,6 +6,8 @@ @inject ILibraryService LibraryService @inject IDialogService DialogService @inject IOptions LibraryOptions +@inject ClientService ClientService +@inject ISnackbar Snackbar Channels diff --git a/Manager.App/Components/Pages/Channels.razor.cs b/Manager.App/Components/Pages/Channels.razor.cs index 99fcfbf..a66bc62 100644 --- a/Manager.App/Components/Pages/Channels.razor.cs +++ b/Manager.App/Components/Pages/Channels.razor.cs @@ -28,21 +28,21 @@ public partial class Channels : ComponentBase return; } - var client = (ClientPrep)result.Data; - if (client == null) + var clientPrep = (ClientPrep)result.Data; + if (clientPrep?.YouTubeClient == null) { return; } - /*var savedResult = await ClientManager.SaveClientAsync(client); + var savedResult = await ClientService.SaveClientAsync(clientPrep.YouTubeClient, clientPrep.Channel); if (!savedResult.IsSuccess) { Snackbar.Add($"Failed to store client: {savedResult.Error?.Description ?? "Unknown!"}", Severity.Error); } else { - Snackbar.Add($"Client {client.External.Channel?.Handle ?? client.Id} saved!", Severity.Success); - }*/ + Snackbar.Add($"Client {clientPrep.Channel?.Handle ?? clientPrep.YouTubeClient.Id} saved!", Severity.Success); + } await InvokeAsync(StateHasChanged); } diff --git a/Manager.App/Components/Pages/Development.razor b/Manager.App/Components/Pages/Development.razor index 668ff32..338665e 100644 --- a/Manager.App/Components/Pages/Development.razor +++ b/Manager.App/Components/Pages/Development.razor @@ -1,6 +1,6 @@ @page "/Development" @using Manager.App.Components.Application.Dev -Development page +Development page diff --git a/Manager.App/Components/Pages/Services.razor b/Manager.App/Components/Pages/Services.razor index 3a3f96f..e9ca48e 100644 --- a/Manager.App/Components/Pages/Services.razor +++ b/Manager.App/Components/Pages/Services.razor @@ -5,7 +5,7 @@ @inject BackgroundServiceRegistry ServiceRegistry -Services +Services @@ -39,4 +39,4 @@ + Elevation="0" Class="mt-3" Style="flex: 1; display: flex; flex-direction: column; min-height: 350px;"/> diff --git a/Manager.App/Components/Routes.razor b/Manager.App/Components/Routes.razor index ae94e9e..608c136 100644 --- a/Manager.App/Components/Routes.razor +++ b/Manager.App/Components/Routes.razor @@ -3,4 +3,5 @@ - \ No newline at end of file + + \ No newline at end of file diff --git a/Manager.App/Services/System/ClientService.cs b/Manager.App/Services/System/ClientService.cs index 89abdc7..42541be 100644 --- a/Manager.App/Services/System/ClientService.cs +++ b/Manager.App/Services/System/ClientService.cs @@ -1,34 +1,32 @@ using System.Net; using DotBased.Logging; using DotBased.Monads; -using Manager.App.Models.Library; using Manager.Data.Entities.LibraryContext; using Manager.YouTube; +using Manager.YouTube.Models.Innertube; namespace Manager.App.Services.System; public class ClientService(IServiceScopeFactory scopeFactory, ILogger logger) - : ExtendedBackgroundService("ClientService", "Managing YouTube clients", logger, TimeSpan.FromMilliseconds(100)) + : ExtendedBackgroundService(nameof(ClientService), "Managing YouTube clients", logger, TimeSpan.FromMinutes(10)) { private readonly List _clients = []; private CancellationToken _cancellationToken; private ILibraryService? _libraryService; - protected override async Task InitializeAsync(CancellationToken stoppingToken) + protected override Task InitializeAsync(CancellationToken stoppingToken) { _cancellationToken = stoppingToken; stoppingToken.Register(CancellationRequested); using var scope = scopeFactory.CreateScope(); _libraryService = scope.ServiceProvider.GetRequiredService(); LogEvent("Initializing service..."); - //Pause(); + return Task.CompletedTask; } - protected override async Task ExecuteServiceAsync(CancellationToken stoppingToken) + protected override Task ExecuteServiceAsync(CancellationToken stoppingToken) { - LogEvent("Sending event..."); - LogEvent("Sending warning event...", LogSeverity.Warning); - LogEvent("Sending error event...", LogSeverity.Error); + return Task.CompletedTask; } private void CancellationRequested() @@ -36,42 +34,54 @@ public class ClientService(IServiceScopeFactory scopeFactory, ILogger> PrepareClient() + public async Task SaveClientAsync(YouTubeClient client, Channel? channelInfo = null, CancellationToken cancellationToken = default) { - - return ResultError.Fail("Not implemented!"); - } + if (_libraryService == null) + { + return ResultError.Fail("Library service is not initialized!."); + } - /*public async Task SaveClientAsync(YouTubeClient client, CancellationToken cancellationToken = default) - { if (string.IsNullOrWhiteSpace(client.Id)) { + LogEvent("Failed to store client no ID!", LogSeverity.Warning); return ResultError.Fail("Client does not have an ID, cannot save to library database!"); } - var channelResult = await libraryService.GetChannelByIdAsync(client.Id, cancellationToken); + var channelResult = await _libraryService.GetChannelByIdAsync(client.Id, cancellationToken); ChannelEntity? channel; - if (channelResult.IsSuccess) + try { - channel = channelResult.Value; - UpdateChannelEntity(channel, client); + if (channelResult.IsSuccess) + { + channel = channelResult.Value; + UpdateChannelEntity(client, channel, channelInfo); + } + else + { + channel = CreateNewChannelFromClient(client, channelInfo); + } } - else + catch (Exception e) { - channel = CreateNewChannelFromClient(client); + LogEvent("Failed to save client: " + e.Message, LogSeverity.Warning); + return ResultError.Error(e); } - var saveResult = await libraryService.SaveChannelAsync(channel, cancellationToken); + var saveResult = await _libraryService.SaveChannelAsync(channel, cancellationToken); return saveResult; - }*/ + } - /*private void UpdateChannelEntity(ChannelEntity channel, YouTubeClient client) + private void UpdateChannelEntity(YouTubeClient client, ChannelEntity entity, Channel? channelInfo) { - channel.Name = client.External.Channel?.ChannelName; - channel.Handle = client.External.Channel?.Handle; - channel.Description = client.External.Channel?.Description; - var clientAcc = channel.ClientAccount; + if (channelInfo != null) + { + entity.Name = channelInfo.ChannelName; + entity.Handle = channelInfo.Handle; + entity.Description = channelInfo.Description; + } + + var clientAcc = entity.ClientAccount; if (clientAcc != null) { clientAcc.UserAgent = clientAcc.UserAgent; @@ -99,8 +109,13 @@ public class ClientService(IServiceScopeFactory scopeFactory, ILogger(); foreach (var cookieObj in client.CookieContainer.GetAllCookies()) { @@ -133,22 +148,21 @@ public class ClientService(IServiceScopeFactory scopeFactory, ILogger> LoadClientByIdAsync(string id) + /*public async Task> LoadClientByIdAsync(string id) { if (string.IsNullOrWhiteSpace(id)) { return ResultError.Fail("Client ID is empty!"); } - return ResultError.Fail("Not implemented"); - } + }*/ } \ No newline at end of file diff --git a/Manager.Data/Contexts/AuditInterceptor.cs b/Manager.Data/Contexts/AuditInterceptor.cs new file mode 100644 index 0000000..401beca --- /dev/null +++ b/Manager.Data/Contexts/AuditInterceptor.cs @@ -0,0 +1,111 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Manager.Data.Entities.Audit; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.ChangeTracking; +using Microsoft.EntityFrameworkCore.Diagnostics; + +namespace Manager.Data.Contexts; + +public class AuditInterceptor : SaveChangesInterceptor +{ + public override InterceptionResult SavingChanges(DbContextEventData eventData, InterceptionResult result) + { + AddHistory(eventData.Context); + return base.SavingChanges(eventData, result); + } + + public override ValueTask> SavingChangesAsync( + DbContextEventData eventData, + InterceptionResult result, + CancellationToken cancellationToken = default) + { + AddHistory(eventData.Context); + return base.SavingChangesAsync(eventData, result, cancellationToken); + } + + private void AddHistory(DbContext? context) + { + if (context == null) return; + + var entries = context.ChangeTracker.Entries() + .Where(e => e.State is EntityState.Modified or EntityState.Deleted or EntityState.Added && Attribute.IsDefined(e.Entity.GetType(), + typeof(AuditableAttribute))); + + var histories = new List(); + + foreach (var entry in entries) + { + var primaryKey = entry.Properties.First(p => p.Metadata.IsPrimaryKey()).CurrentValue?.ToString() ?? "Unknown"; + + var declaredProperties = entry.Entity.GetType() + .GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance) + .Where(p => !Attribute.IsDefined(p.DeclaringType!, typeof(NoAuditAttribute))) + .Select(p => p.Name) + .ToHashSet(); + + var allowedProperties = entry.Properties.Where(p => declaredProperties.Contains(p.Metadata.Name)); + + switch (entry.State) + { + case EntityState.Added: + histories.AddRange(allowedProperties + .Where(p => p.CurrentValue != null) + .Select(p => CreateHistory(entry, p, entry.State, primaryKey)) + ); + break; + case EntityState.Modified: + histories.AddRange(allowedProperties + .Where(p => p.IsModified) + .Select(p => CreateHistory(entry, p, entry.State, primaryKey)) + ); + break; + case EntityState.Deleted: + histories.AddRange(allowedProperties + .Select(p => CreateHistory(entry, p, entry.State, primaryKey)) + ); + break; + } + } + + if (histories.Count != 0) + { + context.Set().AddRange(histories); + } + } + + private EntityHistory CreateHistory(EntityEntry entry, PropertyEntry prop, EntityState changeType, string? primaryKey) + { + return new EntityHistory + { + EntityName = entry.Entity.GetType().Name, + EntityId = primaryKey ?? "Unknown", + PropertyName = prop.Metadata.Name, + OldValue = SerializeValue(prop.OriginalValue), + NewValue = SerializeValue(prop.CurrentValue), + ModifiedUtc = DateTime.UtcNow, + ChangedBy = "SYSTEM", + ChangeType = changeType + }; + } + + private readonly JsonSerializerOptions _jsonSerializerOptions = new() + { + WriteIndented = false, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }; + + private string? SerializeValue(object? value) + { + if (value == null) return null; + + var type = value.GetType(); + + if (type.IsPrimitive || type == typeof(string) || type == typeof(DateTime) || type == typeof(decimal)) + { + return value.ToString(); + } + + return JsonSerializer.Serialize(value, _jsonSerializerOptions); + } +} \ No newline at end of file diff --git a/Manager.Data/Contexts/DateInterceptor.cs b/Manager.Data/Contexts/DateInterceptor.cs new file mode 100644 index 0000000..099ccec --- /dev/null +++ b/Manager.Data/Contexts/DateInterceptor.cs @@ -0,0 +1,38 @@ +using Manager.Data.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; + +namespace Manager.Data.Contexts; + +public class DateInterceptor : SaveChangesInterceptor +{ + public override InterceptionResult SavingChanges(DbContextEventData eventData, InterceptionResult result) + { + UpdateEntryDates(eventData.Context); + return base.SavingChanges(eventData, result); + } + + public override ValueTask> SavingChangesAsync(DbContextEventData eventData, InterceptionResult result, + CancellationToken cancellationToken = new()) + { + UpdateEntryDates(eventData.Context); + return base.SavingChangesAsync(eventData, result, cancellationToken); + } + + private void UpdateEntryDates(DbContext? context) + { + if (context == null) return; + + var entries = context.ChangeTracker.Entries().Where(x => x is { Entity: DateTimeBase, State: EntityState.Added or EntityState.Modified }); + + foreach (var entity in entries) + { + ((DateTimeBase)entity.Entity).LastModifiedUtc = DateTime.UtcNow; + + if (entity.State == EntityState.Added) + { + ((DateTimeBase)entity.Entity).CreatedAtUtc = DateTime.UtcNow; + } + } + } +} \ No newline at end of file diff --git a/Manager.Data/Contexts/LibraryDbContext.cs b/Manager.Data/Contexts/LibraryDbContext.cs index 51c1b3b..4ff3848 100644 --- a/Manager.Data/Contexts/LibraryDbContext.cs +++ b/Manager.Data/Contexts/LibraryDbContext.cs @@ -1,4 +1,4 @@ -using Manager.Data.Entities; +using Manager.Data.Entities.Audit; using Manager.Data.Entities.LibraryContext; using Manager.Data.Entities.LibraryContext.Join; using Microsoft.EntityFrameworkCore; @@ -13,6 +13,8 @@ public sealed class LibraryDbContext : DbContext ChangeTracker.LazyLoadingEnabled = false; Database.EnsureCreated(); } + + public DbSet Histories { get; set; } public DbSet Captions { get; set; } public DbSet Channels { get; set; } @@ -21,9 +23,20 @@ public sealed class LibraryDbContext : DbContext public DbSet Media { get; set; } public DbSet MediaFormats { get; set; } public DbSet Playlists { get; set; } + // Other media (images)? + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.AddInterceptors(new DateInterceptor(), new AuditInterceptor()); + } protected override void OnModelCreating(ModelBuilder modelBuilder) { + modelBuilder.Entity(eh => + { + eh.ToTable("entity_history"); + }); + modelBuilder.Entity(ce => { ce.ToTable("captions"); @@ -101,31 +114,4 @@ public sealed class LibraryDbContext : DbContext base.OnModelCreating(modelBuilder); } - - public override int SaveChanges() - { - UpdateEntryDates(); - return base.SaveChanges(); - } - - public override Task SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = new()) - { - UpdateEntryDates(); - return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken); - } - - private void UpdateEntryDates() - { - var entries = ChangeTracker.Entries().Where(x => x is { Entity: DateTimeBase, State: EntityState.Added or EntityState.Modified }); - - foreach (var entity in entries) - { - ((DateTimeBase)entity.Entity).LastModifiedUtc = DateTime.UtcNow; - - if (entity.State == EntityState.Added) - { - ((DateTimeBase)entity.Entity).CreatedAtUtc = DateTime.UtcNow; - } - } - } } \ No newline at end of file diff --git a/Manager.Data/Entities/Audit/AuditableAttribute.cs b/Manager.Data/Entities/Audit/AuditableAttribute.cs new file mode 100644 index 0000000..df7c5ee --- /dev/null +++ b/Manager.Data/Entities/Audit/AuditableAttribute.cs @@ -0,0 +1,9 @@ +namespace Manager.Data.Entities.Audit; + +/// +/// Make all properties in the entity audible, if they are changed this will be stored as a history in the db. +/// +[AttributeUsage(AttributeTargets.Class)] +public class AuditableAttribute : Attribute +{ +} \ No newline at end of file diff --git a/Manager.Data/Entities/Audit/EntityHistory.cs b/Manager.Data/Entities/Audit/EntityHistory.cs new file mode 100644 index 0000000..a824e78 --- /dev/null +++ b/Manager.Data/Entities/Audit/EntityHistory.cs @@ -0,0 +1,22 @@ +using System.ComponentModel.DataAnnotations; +using Microsoft.EntityFrameworkCore; + +namespace Manager.Data.Entities.Audit; + +public class EntityHistory +{ + [MaxLength(200)] + public required string EntityName { get; set; } + [MaxLength(200)] + public required string EntityId { get; set; } + [MaxLength(200)] + public required string PropertyName { get; set; } + [MaxLength(1000)] + public string? OldValue { get; set; } + [MaxLength(1000)] + public string? NewValue { get; set; } + public DateTime ModifiedUtc { get; set; } = DateTime.UtcNow; + [MaxLength(200)] + public string? ChangedBy { get; set; } + public EntityState ChangeType { get; set; } +} \ No newline at end of file diff --git a/Manager.Data/Entities/Audit/NoAuditAttribute.cs b/Manager.Data/Entities/Audit/NoAuditAttribute.cs new file mode 100644 index 0000000..98d948f --- /dev/null +++ b/Manager.Data/Entities/Audit/NoAuditAttribute.cs @@ -0,0 +1,9 @@ +namespace Manager.Data.Entities.Audit; + +/// +/// Specifies to ignore the properties in the entity to not audit. +/// +[AttributeUsage(AttributeTargets.Class)] +public class NoAuditAttribute : Attribute +{ +} diff --git a/Manager.Data/Entities/DateTimeBase.cs b/Manager.Data/Entities/DateTimeBase.cs index 3638ed8..cc09714 100644 --- a/Manager.Data/Entities/DateTimeBase.cs +++ b/Manager.Data/Entities/DateTimeBase.cs @@ -1,5 +1,8 @@ +using Manager.Data.Entities.Audit; + namespace Manager.Data.Entities; +[NoAudit] public abstract class DateTimeBase { public DateTime CreatedAtUtc { get; set; } = DateTime.UtcNow; diff --git a/Manager.Data/Entities/LibraryContext/CaptionEntity.cs b/Manager.Data/Entities/LibraryContext/CaptionEntity.cs index afaea8d..2fd72be 100644 --- a/Manager.Data/Entities/LibraryContext/CaptionEntity.cs +++ b/Manager.Data/Entities/LibraryContext/CaptionEntity.cs @@ -1,7 +1,9 @@ using System.ComponentModel.DataAnnotations; +using Manager.Data.Entities.Audit; namespace Manager.Data.Entities.LibraryContext; +[Auditable] public class CaptionEntity : DateTimeBase { [MaxLength(DataConstants.DbContext.DefaultDbStringSize)] diff --git a/Manager.Data/Entities/LibraryContext/ChannelEntity.cs b/Manager.Data/Entities/LibraryContext/ChannelEntity.cs index 555358e..bf68f9b 100644 --- a/Manager.Data/Entities/LibraryContext/ChannelEntity.cs +++ b/Manager.Data/Entities/LibraryContext/ChannelEntity.cs @@ -1,7 +1,9 @@ using System.ComponentModel.DataAnnotations; +using Manager.Data.Entities.Audit; namespace Manager.Data.Entities.LibraryContext; +[Auditable] public class ChannelEntity : DateTimeBase { [MaxLength(DataConstants.DbContext.DefaultDbStringSize)] diff --git a/Manager.Data/Entities/LibraryContext/ClientAccountEntity.cs b/Manager.Data/Entities/LibraryContext/ClientAccountEntity.cs index 154c4d0..10c9ae4 100644 --- a/Manager.Data/Entities/LibraryContext/ClientAccountEntity.cs +++ b/Manager.Data/Entities/LibraryContext/ClientAccountEntity.cs @@ -1,7 +1,9 @@ using System.ComponentModel.DataAnnotations; +using Manager.Data.Entities.Audit; namespace Manager.Data.Entities.LibraryContext; +[Auditable] public class ClientAccountEntity : DateTimeBase { [MaxLength(DataConstants.DbContext.DefaultDbStringSize)] diff --git a/Manager.Data/Entities/LibraryContext/HttpCookieEntity.cs b/Manager.Data/Entities/LibraryContext/HttpCookieEntity.cs index c995e4c..0b76d80 100644 --- a/Manager.Data/Entities/LibraryContext/HttpCookieEntity.cs +++ b/Manager.Data/Entities/LibraryContext/HttpCookieEntity.cs @@ -1,9 +1,13 @@ using System.ComponentModel.DataAnnotations; +using Manager.Data.Entities.Audit; namespace Manager.Data.Entities.LibraryContext; +[Auditable] public class HttpCookieEntity : DateTimeBase { + [MaxLength(DataConstants.DbContext.DefaultDbStringSize)] + public required string ClientId { get; set; } [MaxLength(DataConstants.DbContext.DefaultDbStringSize)] public required string Name { get; set; } [MaxLength(DataConstants.DbContext.DefaultDbStringSize)] @@ -15,6 +19,4 @@ public class HttpCookieEntity : DateTimeBase public DateTimeOffset? ExpiresUtc { get; set; } public bool Secure { get; set; } public bool HttpOnly { get; set; } - [MaxLength(DataConstants.DbContext.DefaultDbStringSize)] - public required string ClientId { get; set; } } diff --git a/Manager.Data/Entities/LibraryContext/MediaEntity.cs b/Manager.Data/Entities/LibraryContext/MediaEntity.cs index 7f6f13f..de43600 100644 --- a/Manager.Data/Entities/LibraryContext/MediaEntity.cs +++ b/Manager.Data/Entities/LibraryContext/MediaEntity.cs @@ -1,8 +1,10 @@ using System.ComponentModel.DataAnnotations; +using Manager.Data.Entities.Audit; using Manager.Data.Entities.LibraryContext.Join; namespace Manager.Data.Entities.LibraryContext; +[Auditable] public class MediaEntity : DateTimeBase { [MaxLength(DataConstants.DbContext.DefaultDbStringSize)] @@ -17,7 +19,6 @@ public class MediaEntity : DateTimeBase public List Formats { get; set; } = []; public List Captions { get; set; } = []; public List PlaylistMedias { get; set; } = []; - public MediaExternalState ExternalState { get; set; } = MediaExternalState.Online; public bool IsDownloaded { get; set; } public MediaState State { get; set; } = MediaState.Indexed; diff --git a/Manager.Data/Entities/LibraryContext/PlaylistEntity.cs b/Manager.Data/Entities/LibraryContext/PlaylistEntity.cs index 8f09725..580ed46 100644 --- a/Manager.Data/Entities/LibraryContext/PlaylistEntity.cs +++ b/Manager.Data/Entities/LibraryContext/PlaylistEntity.cs @@ -1,8 +1,10 @@ using System.ComponentModel.DataAnnotations; +using Manager.Data.Entities.Audit; using Manager.Data.Entities.LibraryContext.Join; namespace Manager.Data.Entities.LibraryContext; +[Auditable] public class PlaylistEntity : DateTimeBase { [MaxLength(DataConstants.DbContext.DefaultDbStringSize)] diff --git a/Manager.YouTube/NetworkService.cs b/Manager.YouTube/NetworkService.cs index a37b3e2..4eff9eb 100644 --- a/Manager.YouTube/NetworkService.cs +++ b/Manager.YouTube/NetworkService.cs @@ -31,4 +31,9 @@ public static class NetworkService return ResultError.Error(e); } } + + public static async Task> DownloadBytesAsync(HttpRequestMessage request, YouTubeClient client) + { + return ResultError.Fail("Not implemented"); + } } \ No newline at end of file diff --git a/Manager.YouTube/YouTubeClient.cs b/Manager.YouTube/YouTubeClient.cs index fe29612..daaf66b 100644 --- a/Manager.YouTube/YouTubeClient.cs +++ b/Manager.YouTube/YouTubeClient.cs @@ -204,7 +204,7 @@ public sealed class YouTubeClient : IDisposable public void Dispose() { - HttpClient?.Dispose(); + HttpClient.Dispose(); } private async Task> GetCurrentAccountIdAsync()