Activated contact service for Gmail to retrieve profile picture and sender name.
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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>();
|
||||||
|
|
||||||
|
|||||||
@@ -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>();
|
||||||
|
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user