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> /// </summary>
public string AccountColorHex { get; set; } public string AccountColorHex { get; set; }
/// <summary>
/// Base64 encoded profile picture of the account.
/// </summary>
public string ProfilePictureBase64 { get; set; }
/// <summary> /// <summary>
/// Gets or sets the listing order of the account in the accounts list. /// Gets or sets the listing order of the account in the accounts list.
/// </summary> /// </summary>

View File

@@ -33,7 +33,7 @@ namespace Wino.Core.Domain.Models.Authorization
ClientId = clientId; ClientId = clientId;
// Creates the OAuth 2.0 authorization request. // 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, authorizationEndpoint,
Uri.EscapeDataString(RedirectUri), Uri.EscapeDataString(RedirectUri),
ClientId, ClientId,

View File

@@ -19,6 +19,7 @@ namespace Wino.Core.Integration.Processors
/// </summary> /// </summary>
public interface IDefaultChangeProcessor public interface IDefaultChangeProcessor
{ {
Task UpdateAccountAsync(MailAccount account);
Task<string> UpdateAccountDeltaSynchronizationIdentifierAsync(Guid accountId, string deltaSynchronizationIdentifier); Task<string> UpdateAccountDeltaSynchronizationIdentifierAsync(Guid accountId, string deltaSynchronizationIdentifier);
Task CreateAssignmentAsync(Guid accountId, string mailCopyId, string remoteFolderId); Task CreateAssignmentAsync(Guid accountId, string mailCopyId, string remoteFolderId);
Task DeleteAssignmentAsync(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) public Task UpdateFolderLastSyncDateAsync(Guid folderId)
=> FolderService.UpdateFolderLastSyncDateAsync(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); 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 IEnumerable<MailAccount> HoldingAccounts => new List<MailAccount> { Parameter };
public AccountMenuItem(MailAccount account, IMenuItem parent = null) : base(account, account.Id, parent) public AccountMenuItem(MailAccount account, IMenuItem parent = null) : base(account, account.Id, parent)
@@ -59,6 +65,7 @@ namespace Wino.Core.MenuItems
Parameter = account; Parameter = account;
AccountName = account.Name; AccountName = account.Name;
AttentionReason = account.AttentionReason; AttentionReason = account.AttentionReason;
Base64ProfilePicture = account.ProfilePictureBase64;
if (SubMenuItems == null) return; if (SubMenuItems == null) return;

View File

@@ -3,6 +3,7 @@ using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Net.Http;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging;
@@ -69,8 +70,42 @@ namespace Wino.Core.Synchronizers
/// <param name="cancellationToken">Cancellation token</param> /// <param name="cancellationToken">Cancellation token</param>
public abstract Task ExecuteNativeRequestsAsync(IEnumerable<IRequestBundle<TBaseRequest>> batchedRequests, CancellationToken cancellationToken = default); 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) public async Task<SynchronizationResult> SynchronizeAsync(SynchronizationOptions options, CancellationToken cancellationToken = default)
{ {
try try
@@ -104,6 +139,14 @@ namespace Wino.Core.Synchronizers
await synchronizationSemaphore.WaitAsync(activeSynchronizationCancellationToken); 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. // Let servers to finish their job. Sometimes the servers doesn't respond immediately.
bool shouldDelayExecution = batches.Any(a => a.DelayExecution); bool shouldDelayExecution = batches.Any(a => a.DelayExecution);
@@ -150,6 +193,10 @@ namespace Wino.Core.Synchronizers
private void PublishUnreadItemChanges() private void PublishUnreadItemChanges()
=> WeakReferenceMessenger.Default.Send(new RefreshUnreadCountsMessage(Account.Id)); => 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) public void PublishSynchronizationProgress(double progress)
=> WeakReferenceMessenger.Default.Send(new AccountSynchronizationProgressUpdatedMessage(Account.Id, 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;
using Google.Apis.Gmail.v1.Data; using Google.Apis.Gmail.v1.Data;
using Google.Apis.Http; using Google.Apis.Http;
using Google.Apis.PeopleService.v1;
using Google.Apis.Requests; using Google.Apis.Requests;
using Google.Apis.Services; using Google.Apis.Services;
using MailKit; using MailKit;
@@ -37,8 +38,10 @@ namespace Wino.Core.Synchronizers
// https://github.com/googleapis/google-api-dotnet-client/issues/2603 // https://github.com/googleapis/google-api-dotnet-client/issues/2603
private const uint MaximumAllowedBatchRequestSize = 10; private const uint MaximumAllowedBatchRequestSize = 10;
private readonly ConfigurableHttpClient _gmailHttpClient; private readonly ConfigurableHttpClient _googleHttpClient;
private readonly GmailService _gmailService; private readonly GmailService _gmailService;
private readonly PeopleServiceService _peopleService;
private readonly IAuthenticator _authenticator; private readonly IAuthenticator _authenticator;
private readonly IGmailChangeProcessor _gmailChangeProcessor; private readonly IGmailChangeProcessor _gmailChangeProcessor;
private readonly ILogger _logger = Log.ForContext<GmailSynchronizer>(); private readonly ILogger _logger = Log.ForContext<GmailSynchronizer>();
@@ -54,15 +57,64 @@ namespace Wino.Core.Synchronizers
HttpClientFactory = this HttpClientFactory = this
}; };
_gmailHttpClient = new ConfigurableHttpClient(messageHandler); _googleHttpClient = new ConfigurableHttpClient(messageHandler);
_gmailService = new GmailService(initializer); _gmailService = new GmailService(initializer);
_peopleService = new PeopleServiceService(initializer);
_authenticator = authenticator; _authenticator = authenticator;
_gmailChangeProcessor = gmailChangeProcessor; _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); _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>(); var downloadedMessageIds = new List<string>();

View File

@@ -128,7 +128,7 @@ namespace Wino.Core.Synchronizers
#endregion #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>(); var downloadedMessageIds = new List<string>();

View File

@@ -17,6 +17,7 @@
<PackageReference Include="CommunityToolkit.Diagnostics" Version="8.2.2" /> <PackageReference Include="CommunityToolkit.Diagnostics" Version="8.2.2" />
<PackageReference Include="CommunityToolkit.Mvvm" 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.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="HtmlAgilityPack" Version="1.11.59" />
<PackageReference Include="HtmlKit" Version="1.1.0" /> <PackageReference Include="HtmlKit" Version="1.1.0" />
<PackageReference Include="IsExternalInit" Version="1.0.3"> <PackageReference Include="IsExternalInit" Version="1.0.3">