From 2e9d1d83a40cab83bc102e8b65f0e91e78d03a48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Kaan=20K=C3=B6se?= Date: Tue, 7 Jan 2025 13:42:10 +0100 Subject: [PATCH] Outlook delta synchronization. --- Wino.Calendar.ViewModels/AppShellViewModel.cs | 2 +- Wino.Calendar/Views/CalendarPage.xaml | 3 +- .../Interfaces/ICalendarService.cs | 1 + .../Processors/DefaultChangeProcessor.cs | 9 + .../Synchronizers/OutlookSynchronizer.cs | 155 +++++++++++------- Wino.Services/CalendarService.cs | 10 ++ 6 files changed, 117 insertions(+), 63 deletions(-) diff --git a/Wino.Calendar.ViewModels/AppShellViewModel.cs b/Wino.Calendar.ViewModels/AppShellViewModel.cs index 7d984fab..c24bdc0a 100644 --- a/Wino.Calendar.ViewModels/AppShellViewModel.cs +++ b/Wino.Calendar.ViewModels/AppShellViewModel.cs @@ -221,7 +221,7 @@ namespace Wino.Calendar.ViewModels { var t = new NewCalendarSynchronizationRequested(new CalendarSynchronizationOptions() { - AccountId = Guid.Parse("bd0fc1ab-168a-436d-86ce-0661c0eabaf9"), + AccountId = Guid.Parse("5b2e28bb-3179-4a7f-a62b-373878ee2b53"), Type = CalendarSynchronizationType.CalendarMetadata }, SynchronizationSource.Client); diff --git a/Wino.Calendar/Views/CalendarPage.xaml b/Wino.Calendar/Views/CalendarPage.xaml index 18101fe6..2843cb08 100644 --- a/Wino.Calendar/Views/CalendarPage.xaml +++ b/Wino.Calendar/Views/CalendarPage.xaml @@ -295,7 +295,7 @@ Closed="EventDetailsPopupClosed" DesiredPlacement="{x:Bind calendarHelpers:CalendarXamlHelpers.GetDesiredPlacementModeForEventsDetailsPopup(ViewModel.DisplayDetailsCalendarItemViewModel, ViewModel.StatePersistanceService.CalendarDisplayType), Mode=OneWay}" HorizontalOffset="16" - IsLightDismissEnabled="False" + IsLightDismissEnabled="True" IsOpen="{x:Bind ViewModel.IsEventDetailsVisible, Mode=OneWay}" PlacementTarget="{x:Bind TeachingTipPositionerGrid}" VerticalOffset="16"> @@ -319,7 +319,6 @@ diff --git a/Wino.Core.Domain/Interfaces/ICalendarService.cs b/Wino.Core.Domain/Interfaces/ICalendarService.cs index 8184b89a..64ae1b4a 100644 --- a/Wino.Core.Domain/Interfaces/ICalendarService.cs +++ b/Wino.Core.Domain/Interfaces/ICalendarService.cs @@ -18,5 +18,6 @@ namespace Wino.Core.Domain.Interfaces Task CreateNewCalendarItemAsync(CalendarItem calendarItem, List attendees); Task> GetCalendarEventsAsync(IAccountCalendar calendar, DayRangeRenderModel dayRangeRenderModel); Task GetCalendarItemAsync(Guid accountCalendarId, string remoteEventId); + Task UpdateCalendarDeltaSynchronizationToken(Guid calendarId, string deltaToken); } } diff --git a/Wino.Core/Integration/Processors/DefaultChangeProcessor.cs b/Wino.Core/Integration/Processors/DefaultChangeProcessor.cs index dfad5d8a..5f6e299c 100644 --- a/Wino.Core/Integration/Processors/DefaultChangeProcessor.cs +++ b/Wino.Core/Integration/Processors/DefaultChangeProcessor.cs @@ -49,6 +49,8 @@ namespace Wino.Core.Integration.Processors Task DeleteAccountCalendarAsync(AccountCalendar accountCalendar); Task InsertAccountCalendarAsync(AccountCalendar accountCalendar); Task UpdateAccountCalendarAsync(AccountCalendar accountCalendar); + + Task UpdateCalendarDeltaSynchronizationToken(Guid calendarId, string deltaToken); } public interface IGmailChangeProcessor : IDefaultChangeProcessor @@ -103,7 +105,11 @@ namespace Wino.Core.Integration.Processors /// Account identifier to reset delta token for. /// Empty string to assign account delta sync for. Task ResetAccountDeltaTokenAsync(Guid accountId); + + Task ManageCalendarEventAsync(Microsoft.Graph.Models.Event calendarEvent, AccountCalendar assignedCalendar, MailAccount organizerAccount); + + } public interface IImapChangeProcessor : IDefaultChangeProcessor @@ -199,5 +205,8 @@ namespace Wino.Core.Integration.Processors public Task UpdateAccountCalendarAsync(AccountCalendar accountCalendar) => CalendarService.UpdateAccountCalendarAsync(accountCalendar); + + public Task UpdateCalendarDeltaSynchronizationToken(Guid calendarId, string deltaToken) + => CalendarService.UpdateCalendarDeltaSynchronizationToken(calendarId, deltaToken); } } diff --git a/Wino.Core/Synchronizers/OutlookSynchronizer.cs b/Wino.Core/Synchronizers/OutlookSynchronizer.cs index 97bc17d5..d3640dd3 100644 --- a/Wino.Core/Synchronizers/OutlookSynchronizer.cs +++ b/Wino.Core/Synchronizers/OutlookSynchronizer.cs @@ -966,84 +966,119 @@ namespace Wino.Core.Synchronizers.Mail // await SynchronizeCalendarsAsync(cancellationToken).ConfigureAwait(false); - bool isInitialSync = string.IsNullOrEmpty(Account.CalendarSynchronizationDeltaIdentifier); - var localCalendars = await _outlookChangeProcessor.GetAccountCalendarsAsync(Account.Id).ConfigureAwait(false); - Microsoft.Graph.Me.CalendarView.Delta.DeltaGetResponse eventsDeltaResponse = null; + Microsoft.Graph.Me.Calendars.Item.CalendarView.Delta.DeltaGetResponse eventsDeltaResponse = null; - if (isInitialSync) + // TODO: Maybe we can batch each calendar? + + foreach (var calendar in localCalendars) { - _logger.Debug("No calendar sync identifier for account {Name}. Performing initial sync.", Account.Name); + bool isInitialSync = string.IsNullOrEmpty(calendar.SynchronizationDeltaToken); - var startDate = DateTime.UtcNow.AddYears(-2).ToString("u"); - var endDate = DateTime.UtcNow.ToString("u"); - - // No delta link. Performing initial sync. - eventsDeltaResponse = await _graphClient.Me.CalendarView.Delta.GetAsDeltaGetResponseAsync((requestConfiguration) => + if (isInitialSync) { - requestConfiguration.QueryParameters.StartDateTime = startDate; - requestConfiguration.QueryParameters.EndDateTime = endDate; + _logger.Information("No calendar sync identifier for calendar {Name}. Performing initial sync.", calendar.Name); - // TODO: Expand does not work. - // https://github.com/microsoftgraph/msgraph-sdk-dotnet/issues/2358 + var startDate = DateTime.UtcNow.AddYears(-2).ToString("u"); + var endDate = DateTime.UtcNow.ToString("u"); - requestConfiguration.QueryParameters.Expand = new string[] { "calendar($select=name,id)" }; // Expand the calendar and select name and id. Customize as needed. - }, cancellationToken: cancellationToken); - } - else - { - //var requestInformation = _graphClient.Me.Calendars[calendar.RemoteCalendarId].Events.Delta.ToGetRequestInformation((config) => - //{ - // config.QueryParameters.Top = (int)InitialMessageDownloadCountPerFolder; - // config.QueryParameters.Select = outlookMessageSelectParameters; - // config.QueryParameters.Orderby = ["receivedDateTime desc"]; - //}); + eventsDeltaResponse = await _graphClient.Me.Calendars[calendar.RemoteCalendarId].CalendarView.Delta.GetAsDeltaGetResponseAsync((requestConfiguration) => + { + requestConfiguration.QueryParameters.StartDateTime = startDate; + requestConfiguration.QueryParameters.EndDateTime = endDate; + }, cancellationToken: cancellationToken); - //requestInformation.UrlTemplate = requestInformation.UrlTemplate.Insert(requestInformation.UrlTemplate.Length - 1, ",%24deltatoken"); - //requestInformation.QueryParameters.Add("%24deltatoken", currentDeltaToken); + // No delta link. Performing initial sync. + //eventsDeltaResponse = await _graphClient.Me.CalendarView.Delta.GetAsDeltaGetResponseAsync((requestConfiguration) => + //{ + // requestConfiguration.QueryParameters.StartDateTime = startDate; + // requestConfiguration.QueryParameters.EndDateTime = endDate; - // eventsDeltaResponse = await _graphClient.RequestAdapter.SendAsync(requestInformation, Microsoft.Graph.Me.Calendars.Item.Events.Delta.DeltaGetResponse.CreateFromDiscriminatorValue); - } + // // TODO: Expand does not work. + // // https://github.com/microsoftgraph/msgraph-sdk-dotnet/issues/2358 - List events = new(); - - // We must first save the parent recurring events to not lose exceptions. - // Therefore, order the existing items by their type and save the parent recurring events first. - - var messageIteratorAsync = PageIterator.CreatePageIterator(_graphClient, eventsDeltaResponse, (item) => - { - events.Add(item); - - return true; - }); - - await messageIteratorAsync - .IterateAsync(cancellationToken) - .ConfigureAwait(false); - - // Desc-order will move parent recurring events to the top. - events = events.OrderByDescending(a => a.Type).ToList(); - - foreach (var item in events) - { - try - { - await _handleItemRetrievalSemaphore.WaitAsync(); - //await _outlookChangeProcessor.ManageCalendarEventAsync(item, calendar, Account).ConfigureAwait(false); + // requestConfiguration.QueryParameters.Expand = new string[] { "calendar($select=name,id)" }; // Expand the calendar and select name and id. Customize as needed. + //}, cancellationToken: cancellationToken); } - catch (Exception ex) + else { - // _logger.Error(ex, "Error occurred while handling item {Id} for calendar {Name}", item.Id, calendar.Name); + var currentDeltaToken = calendar.SynchronizationDeltaToken; + + _logger.Information("Performing delta sync for calendar {Name}.", calendar.Name); + + var requestInformation = _graphClient.Me.Calendars[calendar.RemoteCalendarId].CalendarView.Delta.ToGetRequestInformation((requestConfiguration) => + { + + //requestConfiguration.QueryParameters.StartDateTime = startDate; + //requestConfiguration.QueryParameters.EndDateTime = endDate; + }); + + //var requestInformation = _graphClient.Me.Calendars[calendar.RemoteCalendarId].CalendarView.Delta.ToGetRequestInformation((config) => + //{ + // config.QueryParameters.Top = (int)InitialMessageDownloadCountPerFolder; + // config.QueryParameters.Select = outlookMessageSelectParameters; + // config.QueryParameters.Orderby = ["receivedDateTime desc"]; + //}); + + + requestInformation.UrlTemplate = requestInformation.UrlTemplate.Insert(requestInformation.UrlTemplate.Length - 1, ",%24deltatoken"); + requestInformation.QueryParameters.Add("%24deltatoken", currentDeltaToken); + + eventsDeltaResponse = await _graphClient.RequestAdapter.SendAsync(requestInformation, Microsoft.Graph.Me.Calendars.Item.CalendarView.Delta.DeltaGetResponse.CreateFromDiscriminatorValue); } - finally + + List events = new(); + + // We must first save the parent recurring events to not lose exceptions. + // Therefore, order the existing items by their type and save the parent recurring events first. + + var messageIteratorAsync = PageIterator.CreatePageIterator(_graphClient, eventsDeltaResponse, (item) => { - _handleItemRetrievalSemaphore.Release(); + events.Add(item); + + return true; + }); + + await messageIteratorAsync + .IterateAsync(cancellationToken) + .ConfigureAwait(false); + + // Desc-order will move parent recurring events to the top. + events = events.OrderByDescending(a => a.Type).ToList(); + + _logger.Information("Found {Count} events in total.", events.Count); + + foreach (var item in events) + { + try + { + await _handleItemRetrievalSemaphore.WaitAsync(); + await _outlookChangeProcessor.ManageCalendarEventAsync(item, calendar, Account).ConfigureAwait(false); + } + catch (Exception ex) + { + // _logger.Error(ex, "Error occurred while handling item {Id} for calendar {Name}", item.Id, calendar.Name); + } + finally + { + _handleItemRetrievalSemaphore.Release(); + } + } + + var latestDeltaLink = messageIteratorAsync.Deltalink; + + //Store delta link for tracking new changes. + if (!string.IsNullOrEmpty(latestDeltaLink)) + { + // Parse Delta Token from Delta Link since v5 of Graph SDK works based on the token, not the link. + + var deltaToken = GetDeltaTokenFromDeltaLink(latestDeltaLink); + + await _outlookChangeProcessor.UpdateCalendarDeltaSynchronizationToken(calendar.Id, deltaToken).ConfigureAwait(false); } } - // latestDeltaLink = messageIteratorAsync.Deltalink; - return default; } diff --git a/Wino.Services/CalendarService.cs b/Wino.Services/CalendarService.cs index 65ba5d58..6b405269 100644 --- a/Wino.Services/CalendarService.cs +++ b/Wino.Services/CalendarService.cs @@ -198,5 +198,15 @@ namespace Wino.Services return calendarItem; } + + public Task UpdateCalendarDeltaSynchronizationToken(Guid calendarId, string deltaToken) + { + var query = new Query() + .From(nameof(AccountCalendar)) + .Where(nameof(AccountCalendar.Id), calendarId) + .AsUpdate(new { SynchronizationDeltaToken = deltaToken }); + + return Connection.ExecuteAsync(query.GetRawQuery()); + } } }