Initial integration.
This commit is contained in:
@@ -177,6 +177,15 @@ public static class GoogleIntegratorExtensions
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the timezone string from EventDateTime.
|
||||
/// Returns null for all-day events or if timezone is not specified.
|
||||
/// </summary>
|
||||
public static string GetEventTimeZone(EventDateTime eventDateTime)
|
||||
{
|
||||
return eventDateTime?.TimeZone;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// RRULE, EXRULE, RDATE and EXDATE lines for a recurring event, as specified in RFC5545.
|
||||
/// </summary>
|
||||
|
||||
@@ -229,23 +229,37 @@ public static class OutlookIntegratorExtensions
|
||||
|
||||
public static DateTimeOffset GetDateTimeOffsetFromDateTimeTimeZone(DateTimeTimeZone dateTimeTimeZone)
|
||||
{
|
||||
if (dateTimeTimeZone == null || string.IsNullOrEmpty(dateTimeTimeZone.DateTime) || string.IsNullOrEmpty(dateTimeTimeZone.TimeZone))
|
||||
if (dateTimeTimeZone == null || string.IsNullOrEmpty(dateTimeTimeZone.DateTime))
|
||||
{
|
||||
throw new ArgumentException("DateTimeTimeZone is null or empty.");
|
||||
throw new ArgumentException("DateTimeTimeZone or DateTime is null or empty.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Parse the DateTime string
|
||||
if (DateTime.TryParse(dateTimeTimeZone.DateTime, out DateTime parsedDateTime))
|
||||
if (!DateTime.TryParse(dateTimeTimeZone.DateTime, out DateTime parsedDateTime))
|
||||
{
|
||||
throw new ArgumentException("DateTime string is not in a valid format.");
|
||||
}
|
||||
|
||||
// If no timezone is provided, assume UTC
|
||||
if (string.IsNullOrEmpty(dateTimeTimeZone.TimeZone))
|
||||
{
|
||||
return new DateTimeOffset(parsedDateTime, TimeSpan.Zero);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Get TimeZoneInfo to get the offset
|
||||
TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(dateTimeTimeZone.TimeZone);
|
||||
TimeSpan offset = timeZoneInfo.GetUtcOffset(parsedDateTime);
|
||||
return new DateTimeOffset(parsedDateTime, offset);
|
||||
}
|
||||
else
|
||||
throw new ArgumentException("DateTime string is not in a valid format.");
|
||||
catch (TimeZoneNotFoundException)
|
||||
{
|
||||
// If timezone is not found, assume UTC as fallback
|
||||
return new DateTimeOffset(parsedDateTime, TimeSpan.Zero);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
|
||||
@@ -102,6 +102,10 @@ public class GmailChangeProcessor : DefaultChangeProcessor, IGmailChangeProcesso
|
||||
DurationInSeconds = totalDurationInSeconds,
|
||||
Location = string.IsNullOrEmpty(calendarEvent.Location) ? parentRecurringEvent.Location : calendarEvent.Location,
|
||||
|
||||
// Store timezone information
|
||||
StartTimeZone = GoogleIntegratorExtensions.GetEventTimeZone(calendarEvent.Start) ?? parentRecurringEvent.StartTimeZone,
|
||||
EndTimeZone = GoogleIntegratorExtensions.GetEventTimeZone(calendarEvent.End) ?? parentRecurringEvent.EndTimeZone,
|
||||
|
||||
// Leave it empty if it's not populated.
|
||||
Recurrence = GoogleIntegratorExtensions.GetRecurrenceString(calendarEvent) == null ? string.Empty : GoogleIntegratorExtensions.GetRecurrenceString(calendarEvent),
|
||||
Status = GetStatus(calendarEvent.Status),
|
||||
@@ -137,6 +141,11 @@ public class GmailChangeProcessor : DefaultChangeProcessor, IGmailChangeProcesso
|
||||
EndDateOffset = eventEndDateTimeOffset.Value.Offset,
|
||||
DurationInSeconds = totalDurationInSeconds,
|
||||
Location = calendarEvent.Location,
|
||||
|
||||
// Store timezone information from Google Calendar event
|
||||
StartTimeZone = GoogleIntegratorExtensions.GetEventTimeZone(calendarEvent.Start),
|
||||
EndTimeZone = GoogleIntegratorExtensions.GetEventTimeZone(calendarEvent.End),
|
||||
|
||||
Recurrence = GoogleIntegratorExtensions.GetRecurrenceString(calendarEvent),
|
||||
Status = GetStatus(calendarEvent.Status),
|
||||
Title = calendarEvent.Summary,
|
||||
|
||||
@@ -69,6 +69,11 @@ public class OutlookChangeProcessor(IDatabaseService databaseService,
|
||||
savingItem.EndDateOffset = eventEndDateTimeOffset.Offset;
|
||||
savingItem.DurationInSeconds = durationInSeconds;
|
||||
|
||||
// Store the timezone information from the event
|
||||
// This preserves the original timezone from Outlook, allowing proper reconstruction later
|
||||
savingItem.StartTimeZone = calendarEvent.Start?.TimeZone;
|
||||
savingItem.EndTimeZone = calendarEvent.End?.TimeZone;
|
||||
|
||||
savingItem.Title = calendarEvent.Subject;
|
||||
savingItem.Description = calendarEvent.Body?.Content;
|
||||
savingItem.Location = calendarEvent.Location?.DisplayName;
|
||||
@@ -103,6 +108,34 @@ public class OutlookChangeProcessor(IDatabaseService databaseService,
|
||||
savingItem.OrganizerDisplayName = calendarEvent.Organizer?.EmailAddress?.Name;
|
||||
savingItem.IsHidden = false;
|
||||
|
||||
// Set timestamps
|
||||
if (calendarEvent.CreatedDateTime.HasValue)
|
||||
savingItem.CreatedAt = calendarEvent.CreatedDateTime.Value;
|
||||
|
||||
if (calendarEvent.LastModifiedDateTime.HasValue)
|
||||
savingItem.UpdatedAt = calendarEvent.LastModifiedDateTime.Value;
|
||||
|
||||
// Set visibility
|
||||
if (calendarEvent.Sensitivity != null)
|
||||
{
|
||||
savingItem.Visibility = calendarEvent.Sensitivity.Value switch
|
||||
{
|
||||
Sensitivity.Normal => CalendarItemVisibility.Public,
|
||||
Sensitivity.Personal => CalendarItemVisibility.Private,
|
||||
Sensitivity.Private => CalendarItemVisibility.Private,
|
||||
Sensitivity.Confidential => CalendarItemVisibility.Confidential,
|
||||
_ => CalendarItemVisibility.Public
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
savingItem.Visibility = CalendarItemVisibility.Public;
|
||||
}
|
||||
|
||||
// Set IsLocked based on whether the user is the organizer
|
||||
// Read-only events are those where the current user is not the organizer
|
||||
savingItem.IsLocked = calendarEvent.IsOrganizer.HasValue && !calendarEvent.IsOrganizer.Value;
|
||||
|
||||
if (calendarEvent.ResponseStatus?.Response != null)
|
||||
{
|
||||
switch (calendarEvent.ResponseStatus.Response.Value)
|
||||
|
||||
@@ -299,6 +299,56 @@ public class SynchronizationManager : ISynchronizationManager
|
||||
return await SynchronizeMailAsync(options, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles calendar synchronization for the given account.
|
||||
/// </summary>
|
||||
/// <param name="options">Calendar synchronization options</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Synchronization result</returns>
|
||||
public async Task<CalendarSynchronizationResult> SynchronizeCalendarAsync(CalendarSynchronizationOptions options,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
EnsureInitialized();
|
||||
|
||||
var synchronizer = await GetOrCreateSynchronizerAsync(options.AccountId);
|
||||
if (synchronizer == null)
|
||||
{
|
||||
_logger.Error("Could not find or create synchronizer for account {AccountId}", options.AccountId);
|
||||
return CalendarSynchronizationResult.Failed;
|
||||
}
|
||||
|
||||
_logger.Information("Starting calendar synchronization for account {AccountId} with type {SyncType}",
|
||||
options.AccountId, options.Type);
|
||||
|
||||
try
|
||||
{
|
||||
var result = await synchronizer.SynchronizeCalendarEventsAsync(options, cancellationToken);
|
||||
|
||||
_logger.Information("Calendar synchronization completed for account {AccountId} with state {State}",
|
||||
options.AccountId, result.CompletedState);
|
||||
|
||||
// TODO: Create notifications for new calendar events when INotificationBuilder supports it
|
||||
// if (result.DownloadedEvents?.Any() ?? false)
|
||||
// await _notificationBuilder.CreateCalendarNotificationsAsync(result.DownloadedEvents);
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (AuthenticationAttentionException authEx)
|
||||
{
|
||||
_logger.Warning("Account {AccountId} requires attention due to authentication issues", options.AccountId);
|
||||
|
||||
// Create app notification for authentication attention
|
||||
_notificationBuilder.CreateAttentionRequiredNotification(authEx.Account);
|
||||
|
||||
return CalendarSynchronizationResult.Failed;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Calendar synchronization failed for account {AccountId}", options.AccountId);
|
||||
return CalendarSynchronizationResult.Failed;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Downloads a MIME message for the given mail item.
|
||||
/// </summary>
|
||||
|
||||
@@ -1631,8 +1631,8 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
||||
{
|
||||
_logger.Information("No calendar sync identifier for calendar {Name}. Performing initial sync.", calendar.Name);
|
||||
|
||||
var startDate = DateTime.UtcNow.AddYears(-2).ToString("u");
|
||||
var endDate = DateTime.UtcNow.ToString("u");
|
||||
var startDate = DateTimeOffset.UtcNow.AddYears(-2).ToString("o");
|
||||
var endDate = DateTimeOffset.UtcNow.ToString("o");
|
||||
|
||||
eventsDeltaResponse = await _graphClient.Me.Calendars[calendar.RemoteCalendarId].CalendarView.Delta.GetAsDeltaGetResponseAsync((requestConfiguration) =>
|
||||
{
|
||||
@@ -1658,20 +1658,7 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
||||
|
||||
_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"];
|
||||
//});
|
||||
|
||||
var requestInformation = _graphClient.Me.Calendars[calendar.RemoteCalendarId].CalendarView.Delta.ToGetRequestInformation();
|
||||
|
||||
requestInformation.UrlTemplate = requestInformation.UrlTemplate.Insert(requestInformation.UrlTemplate.Length - 1, ",%24deltatoken");
|
||||
requestInformation.QueryParameters.Add("%24deltatoken", currentDeltaToken);
|
||||
@@ -1726,11 +1713,12 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
||||
|
||||
var deltaToken = GetDeltaTokenFromDeltaLink(latestDeltaLink);
|
||||
|
||||
await _outlookChangeProcessor.UpdateCalendarDeltaSynchronizationToken(calendar.Id, deltaToken).ConfigureAwait(false);
|
||||
/// await _outlookChangeProcessor.UpdateCalendarDeltaSynchronizationToken(calendar.Id, deltaToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
// TODO: Return proper results.
|
||||
return CalendarSynchronizationResult.Empty;
|
||||
}
|
||||
|
||||
private async Task SynchronizeCalendarsAsync(CancellationToken cancellationToken = default)
|
||||
|
||||
Reference in New Issue
Block a user