From 8f66fcbb00b2ca335e6b4a9d9f715dfb20bb9c3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Kaan=20K=C3=B6se?= Date: Thu, 15 Aug 2024 23:57:45 +0200 Subject: [PATCH] Activated contact service for Gmail to retrieve profile picture and sender name. --- Wino.Core.Domain/Entities/MailAccount.cs | 5 ++ .../GoogleAuthorizationRequest.cs | 2 +- .../Processors/DefaultChangeProcessor.cs | 4 ++ Wino.Core/MenuItems/AccountMenuItem.cs | 7 +++ Wino.Core/Synchronizers/BaseSynchronizer.cs | 49 ++++++++++++++- Wino.Core/Synchronizers/GmailSynchronizer.cs | 60 +++++++++++++++++-- Wino.Core/Synchronizers/ImapSynchronizer.cs | 2 +- .../Synchronizers/OutlookSynchronizer.cs | 2 +- Wino.Core/Wino.Core.csproj | 1 + 9 files changed, 124 insertions(+), 8 deletions(-) diff --git a/Wino.Core.Domain/Entities/MailAccount.cs b/Wino.Core.Domain/Entities/MailAccount.cs index ecd8fb64..60568d32 100644 --- a/Wino.Core.Domain/Entities/MailAccount.cs +++ b/Wino.Core.Domain/Entities/MailAccount.cs @@ -45,6 +45,11 @@ namespace Wino.Core.Domain.Entities /// public string AccountColorHex { get; set; } + /// + /// Base64 encoded profile picture of the account. + /// + public string ProfilePictureBase64 { get; set; } + /// /// Gets or sets the listing order of the account in the accounts list. /// diff --git a/Wino.Core.Domain/Models/Authorization/GoogleAuthorizationRequest.cs b/Wino.Core.Domain/Models/Authorization/GoogleAuthorizationRequest.cs index 455fff47..bef11d59 100644 --- a/Wino.Core.Domain/Models/Authorization/GoogleAuthorizationRequest.cs +++ b/Wino.Core.Domain/Models/Authorization/GoogleAuthorizationRequest.cs @@ -33,7 +33,7 @@ namespace Wino.Core.Domain.Models.Authorization ClientId = clientId; // Creates the OAuth 2.0 authorization request. - return string.Format("{0}?response_type=code&scope=https://mail.google.com/ https://www.googleapis.com/auth/gmail.labels&redirect_uri={1}&client_id={2}&state={3}&code_challenge={4}&code_challenge_method={5}", + return string.Format("{0}?response_type=code&scope=https://mail.google.com/ https://www.googleapis.com/auth/gmail.labels https://www.googleapis.com/auth/userinfo.profile&redirect_uri={1}&client_id={2}&state={3}&code_challenge={4}&code_challenge_method={5}", authorizationEndpoint, Uri.EscapeDataString(RedirectUri), ClientId, diff --git a/Wino.Core/Integration/Processors/DefaultChangeProcessor.cs b/Wino.Core/Integration/Processors/DefaultChangeProcessor.cs index 14ebc1de..4028dc9c 100644 --- a/Wino.Core/Integration/Processors/DefaultChangeProcessor.cs +++ b/Wino.Core/Integration/Processors/DefaultChangeProcessor.cs @@ -19,6 +19,7 @@ namespace Wino.Core.Integration.Processors /// public interface IDefaultChangeProcessor { + Task UpdateAccountAsync(MailAccount account); Task UpdateAccountDeltaSynchronizationIdentifierAsync(Guid accountId, string deltaSynchronizationIdentifier); Task CreateAssignmentAsync(Guid accountId, string mailCopyId, string remoteFolderId); Task DeleteAssignmentAsync(Guid accountId, string mailCopyId, string remoteFolderId); @@ -172,5 +173,8 @@ namespace Wino.Core.Integration.Processors public Task UpdateFolderLastSyncDateAsync(Guid folderId) => FolderService.UpdateFolderLastSyncDateAsync(folderId); + + public Task UpdateAccountAsync(MailAccount account) + => AccountService.UpdateAccountAsync(account); } } diff --git a/Wino.Core/MenuItems/AccountMenuItem.cs b/Wino.Core/MenuItems/AccountMenuItem.cs index 045038f4..5cba23d3 100644 --- a/Wino.Core/MenuItems/AccountMenuItem.cs +++ b/Wino.Core/MenuItems/AccountMenuItem.cs @@ -47,6 +47,12 @@ namespace Wino.Core.MenuItems set => SetProperty(Parameter.Name, value, Parameter, (u, n) => u.Name = n); } + public string Base64ProfilePicture + { + get => Parameter.Name; + set => SetProperty(Parameter.ProfilePictureBase64, value, Parameter, (u, n) => u.ProfilePictureBase64 = n); + } + public IEnumerable HoldingAccounts => new List { Parameter }; public AccountMenuItem(MailAccount account, IMenuItem parent = null) : base(account, account.Id, parent) @@ -59,6 +65,7 @@ namespace Wino.Core.MenuItems Parameter = account; AccountName = account.Name; AttentionReason = account.AttentionReason; + Base64ProfilePicture = account.ProfilePictureBase64; if (SubMenuItems == null) return; diff --git a/Wino.Core/Synchronizers/BaseSynchronizer.cs b/Wino.Core/Synchronizers/BaseSynchronizer.cs index acd7f213..055a4bb4 100644 --- a/Wino.Core/Synchronizers/BaseSynchronizer.cs +++ b/Wino.Core/Synchronizers/BaseSynchronizer.cs @@ -3,6 +3,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; using CommunityToolkit.Mvvm.Messaging; @@ -69,8 +70,42 @@ namespace Wino.Core.Synchronizers /// Cancellation token public abstract Task ExecuteNativeRequestsAsync(IEnumerable> batchedRequests, CancellationToken cancellationToken = default); - public abstract Task SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default); + /// + /// Refreshed remote mail account profile if possible. + /// Aliases, profile pictures, mailbox settings will be handled in this step. + /// + protected virtual Task SynchronizeProfileInformationAsync() => Task.CompletedTask; + /// + /// Returns the base64 encoded profile picture of the account from the given URL. + /// + /// URL to retrieve picture from. + /// base64 encoded profile picture + protected async Task GetProfilePictureBase64EncodedAsync(string url) + { + using var client = new HttpClient(); + + var response = await client.GetAsync(url).ConfigureAwait(false); + var byteContent = await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false); + + return Convert.ToBase64String(byteContent); + } + + /// + /// Internally synchronizes the account with the given options. + /// Not exposed and overriden for each synchronizer. + /// + /// Synchronization options. + /// Cancellation token. + /// Synchronization result that contains summary of the sync. + protected abstract Task SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default); + + /// + /// Batches network requests, executes them, and does the needed synchronization after the batch request execution. + /// + /// Synchronization options. + /// Cancellation token. + /// Synchronization result that contains summary of the sync. public async Task SynchronizeAsync(SynchronizationOptions options, CancellationToken cancellationToken = default) { try @@ -104,6 +139,14 @@ namespace Wino.Core.Synchronizers await synchronizationSemaphore.WaitAsync(activeSynchronizationCancellationToken); + if (options.Type == SynchronizationType.Full) + { + // Refresh profile information and mailbox settings on full synchronization. + // Exceptions here is not critical. Therefore, they are ignored. + + await SynchronizeProfileInformationAsync(); + } + // Let servers to finish their job. Sometimes the servers doesn't respond immediately. bool shouldDelayExecution = batches.Any(a => a.DelayExecution); @@ -150,6 +193,10 @@ namespace Wino.Core.Synchronizers private void PublishUnreadItemChanges() => WeakReferenceMessenger.Default.Send(new RefreshUnreadCountsMessage(Account.Id)); + /// + /// Sends a message to the shell to update the synchronization progress. + /// + /// Percentage of the progress. public void PublishSynchronizationProgress(double progress) => WeakReferenceMessenger.Default.Send(new AccountSynchronizationProgressUpdatedMessage(Account.Id, progress)); diff --git a/Wino.Core/Synchronizers/GmailSynchronizer.cs b/Wino.Core/Synchronizers/GmailSynchronizer.cs index 3ee7e044..106a8413 100644 --- a/Wino.Core/Synchronizers/GmailSynchronizer.cs +++ b/Wino.Core/Synchronizers/GmailSynchronizer.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Google.Apis.Gmail.v1; using Google.Apis.Gmail.v1.Data; using Google.Apis.Http; +using Google.Apis.PeopleService.v1; using Google.Apis.Requests; using Google.Apis.Services; using MailKit; @@ -37,8 +38,10 @@ namespace Wino.Core.Synchronizers // https://github.com/googleapis/google-api-dotnet-client/issues/2603 private const uint MaximumAllowedBatchRequestSize = 10; - private readonly ConfigurableHttpClient _gmailHttpClient; + private readonly ConfigurableHttpClient _googleHttpClient; private readonly GmailService _gmailService; + private readonly PeopleServiceService _peopleService; + private readonly IAuthenticator _authenticator; private readonly IGmailChangeProcessor _gmailChangeProcessor; private readonly ILogger _logger = Log.ForContext(); @@ -54,15 +57,64 @@ namespace Wino.Core.Synchronizers HttpClientFactory = this }; - _gmailHttpClient = new ConfigurableHttpClient(messageHandler); + _googleHttpClient = new ConfigurableHttpClient(messageHandler); + _gmailService = new GmailService(initializer); + _peopleService = new PeopleServiceService(initializer); + _authenticator = authenticator; _gmailChangeProcessor = gmailChangeProcessor; } - public ConfigurableHttpClient CreateHttpClient(CreateHttpClientArgs args) => _gmailHttpClient; + public ConfigurableHttpClient CreateHttpClient(CreateHttpClientArgs args) => _googleHttpClient; - public override async Task SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default) + protected override async Task SynchronizeProfileInformationAsync() + { + // Gmail profile info synchronizes Alias and Profile Picture. + + try + { + var profileRequest = _peopleService.People.Get("people/me"); + profileRequest.PersonFields = "names,photos"; + + string senderName = Account.SenderName, base64ProfilePicture = Account.ProfilePictureBase64; + + var userProfile = await profileRequest.ExecuteAsync(); + + senderName = userProfile.Names?.FirstOrDefault()?.DisplayName ?? Account.SenderName; + + var profilePicture = userProfile.Photos?.FirstOrDefault()?.Url ?? string.Empty; + + if (!string.IsNullOrEmpty(profilePicture)) + { + base64ProfilePicture = await GetProfilePictureBase64EncodedAsync(profilePicture).ConfigureAwait(false); + } + + bool shouldUpdateAccountProfile = (!string.IsNullOrEmpty(senderName) && Account.SenderName != senderName) + || (!string.IsNullOrEmpty(profilePicture) && Account.ProfilePictureBase64 != base64ProfilePicture); + + if (!string.IsNullOrEmpty(senderName) && Account.SenderName != senderName) + { + Account.SenderName = senderName; + } + + if (!string.IsNullOrEmpty(profilePicture) && Account.ProfilePictureBase64 != base64ProfilePicture) + { + Account.ProfilePictureBase64 = base64ProfilePicture; + } + + if (shouldUpdateAccountProfile) + { + await _gmailChangeProcessor.UpdateAccountAsync(Account).ConfigureAwait(false); + } + } + catch (Exception ex) + { + Logger.Error(ex, "Error while synchronizing profile information for {Name}", Account.Name); + } + } + + protected override async Task SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default) { _logger.Information("Internal synchronization started for {Name}", Account.Name); diff --git a/Wino.Core/Synchronizers/ImapSynchronizer.cs b/Wino.Core/Synchronizers/ImapSynchronizer.cs index 0b8cd2f6..e0ef6810 100644 --- a/Wino.Core/Synchronizers/ImapSynchronizer.cs +++ b/Wino.Core/Synchronizers/ImapSynchronizer.cs @@ -405,7 +405,7 @@ namespace Wino.Core.Synchronizers ]; } - public override async Task SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default) + protected override async Task SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default) { var downloadedMessageIds = new List(); diff --git a/Wino.Core/Synchronizers/OutlookSynchronizer.cs b/Wino.Core/Synchronizers/OutlookSynchronizer.cs index 554e9dcf..c9872d3f 100644 --- a/Wino.Core/Synchronizers/OutlookSynchronizer.cs +++ b/Wino.Core/Synchronizers/OutlookSynchronizer.cs @@ -128,7 +128,7 @@ namespace Wino.Core.Synchronizers #endregion - public override async Task SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default) + protected override async Task SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default) { var downloadedMessageIds = new List(); diff --git a/Wino.Core/Wino.Core.csproj b/Wino.Core/Wino.Core.csproj index be8ca55c..476216e3 100644 --- a/Wino.Core/Wino.Core.csproj +++ b/Wino.Core/Wino.Core.csproj @@ -17,6 +17,7 @@ +