Activated contact service for Gmail to retrieve profile picture and sender name.

This commit is contained in:
Burak Kaan Köse
2024-08-15 23:57:45 +02:00
parent fe449ee1f3
commit 8f66fcbb00
9 changed files with 124 additions and 8 deletions

View File

@@ -45,6 +45,11 @@ namespace Wino.Core.Domain.Entities
/// </summary>
public string AccountColorHex { get; set; }
/// <summary>
/// Base64 encoded profile picture of the account.
/// </summary>
public string ProfilePictureBase64 { get; set; }
/// <summary>
/// Gets or sets the listing order of the account in the accounts list.
/// </summary>

View File

@@ -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,

View File

@@ -19,6 +19,7 @@ namespace Wino.Core.Integration.Processors
/// </summary>
public interface IDefaultChangeProcessor
{
Task UpdateAccountAsync(MailAccount account);
Task<string> 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);
}
}

View File

@@ -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<MailAccount> HoldingAccounts => new List<MailAccount> { 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;

View File

@@ -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
/// <param name="cancellationToken">Cancellation token</param>
public abstract Task ExecuteNativeRequestsAsync(IEnumerable<IRequestBundle<TBaseRequest>> batchedRequests, CancellationToken cancellationToken = default);
public abstract Task<SynchronizationResult> SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default);
/// <summary>
/// Refreshed remote mail account profile if possible.
/// Aliases, profile pictures, mailbox settings will be handled in this step.
/// </summary>
protected virtual Task SynchronizeProfileInformationAsync() => Task.CompletedTask;
/// <summary>
/// Returns the base64 encoded profile picture of the account from the given URL.
/// </summary>
/// <param name="url">URL to retrieve picture from.</param>
/// <returns>base64 encoded profile picture</returns>
protected async Task<string> 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);
}
/// <summary>
/// Internally synchronizes the account with the given options.
/// Not exposed and overriden for each synchronizer.
/// </summary>
/// <param name="options">Synchronization options.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Synchronization result that contains summary of the sync.</returns>
protected abstract Task<SynchronizationResult> SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default);
/// <summary>
/// Batches network requests, executes them, and does the needed synchronization after the batch request execution.
/// </summary>
/// <param name="options">Synchronization options.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Synchronization result that contains summary of the sync.</returns>
public async Task<SynchronizationResult> 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));
/// <summary>
/// Sends a message to the shell to update the synchronization progress.
/// </summary>
/// <param name="progress">Percentage of the progress.</param>
public void PublishSynchronizationProgress(double progress)
=> WeakReferenceMessenger.Default.Send(new AccountSynchronizationProgressUpdatedMessage(Account.Id, progress));

View File

@@ -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<GmailSynchronizer>();
@@ -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<SynchronizationResult> 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<SynchronizationResult> SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default)
{
_logger.Information("Internal synchronization started for {Name}", Account.Name);

View File

@@ -405,7 +405,7 @@ namespace Wino.Core.Synchronizers
];
}
public override async Task<SynchronizationResult> SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default)
protected override async Task<SynchronizationResult> SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default)
{
var downloadedMessageIds = new List<string>();

View File

@@ -128,7 +128,7 @@ namespace Wino.Core.Synchronizers
#endregion
public override async Task<SynchronizationResult> SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default)
protected override async Task<SynchronizationResult> SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default)
{
var downloadedMessageIds = new List<string>();

View File

@@ -17,6 +17,7 @@
<PackageReference Include="CommunityToolkit.Diagnostics" Version="8.2.2" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
<PackageReference Include="Google.Apis.Gmail.v1" Version="1.68.0.3427" />
<PackageReference Include="Google.Apis.PeopleService.v1" Version="1.68.0.3359" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.59" />
<PackageReference Include="HtmlKit" Version="1.1.0" />
<PackageReference Include="IsExternalInit" Version="1.0.3">