Refactored the calendar synchronization code using AI.
This commit is contained in:
@@ -5,7 +5,6 @@ using System.Web;
|
||||
using Google.Apis.Calendar.v3.Data;
|
||||
using Google.Apis.Gmail.v1.Data;
|
||||
using MimeKit;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Entities.Calendar;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Enums;
|
||||
@@ -179,6 +178,11 @@ public static class GoogleIntegratorExtensions
|
||||
Id = Guid.NewGuid(),
|
||||
TimeZone = calendarListEntry.TimeZone,
|
||||
IsPrimary = calendarListEntry.Primary.GetValueOrDefault(),
|
||||
Description = calendarListEntry.Description,
|
||||
AccessRole = calendarListEntry.AccessRole,
|
||||
CreatedDate = DateTime.UtcNow,
|
||||
LastSyncTime = DateTime.UtcNow,
|
||||
Location = calendarListEntry.Location,
|
||||
};
|
||||
|
||||
// Bg color must present. Generate one if doesnt exists.
|
||||
@@ -190,42 +194,121 @@ public static class GoogleIntegratorExtensions
|
||||
return calendar;
|
||||
}
|
||||
|
||||
public static DateTimeOffset? GetEventDateTimeOffset(EventDateTime calendarEvent)
|
||||
public static CalendarItem MapGoogleEventToCalendarEvent(this Event googleEvent, AccountCalendar calendar)
|
||||
{
|
||||
if (calendarEvent != null)
|
||||
var calendarEvent = new CalendarItem
|
||||
{
|
||||
if (calendarEvent.DateTimeDateTimeOffset != null)
|
||||
RemoteEventId = googleEvent.Id,
|
||||
CalendarId = calendar.Id, // Use internal Guid
|
||||
Title = googleEvent.Summary ?? string.Empty,
|
||||
Description = googleEvent.Description,
|
||||
Location = googleEvent.Location,
|
||||
Status = googleEvent.Status,
|
||||
RecurringEventId = googleEvent.RecurringEventId
|
||||
};
|
||||
|
||||
// Handle start and end times
|
||||
if (googleEvent.Start != null)
|
||||
{
|
||||
if (googleEvent.Start.Date != null)
|
||||
{
|
||||
return calendarEvent.DateTimeDateTimeOffset.Value;
|
||||
calendarEvent.IsAllDay = true;
|
||||
calendarEvent.StartDateTime = DateTime.Parse(googleEvent.Start.Date);
|
||||
}
|
||||
else if (calendarEvent.Date != null)
|
||||
else if (googleEvent.Start.DateTimeDateTimeOffset.HasValue)
|
||||
{
|
||||
if (DateTime.TryParse(calendarEvent.Date, out DateTime eventDateTime))
|
||||
{
|
||||
// Date-only events are treated as UTC midnight
|
||||
return new DateTimeOffset(eventDateTime, TimeSpan.Zero);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception("Invalid date format in Google Calendar event date.");
|
||||
}
|
||||
calendarEvent.IsAllDay = false;
|
||||
calendarEvent.StartDateTime = googleEvent.Start.DateTimeDateTimeOffset.Value.DateTime;
|
||||
calendarEvent.TimeZone = googleEvent.Start.TimeZone;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
if (googleEvent.End != null)
|
||||
{
|
||||
if (googleEvent.End.Date != null)
|
||||
{
|
||||
calendarEvent.EndDateTime = DateTime.Parse(googleEvent.End.Date);
|
||||
}
|
||||
else if (googleEvent.End.DateTimeDateTimeOffset.HasValue)
|
||||
{
|
||||
calendarEvent.EndDateTime = googleEvent.End.DateTimeDateTimeOffset.Value.DateTime;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle recurrence rules
|
||||
if (googleEvent.Recurrence != null && googleEvent.Recurrence.Count > 0)
|
||||
{
|
||||
calendarEvent.RecurrenceRules = string.Join(";", googleEvent.Recurrence);
|
||||
}
|
||||
|
||||
// Handle organizer
|
||||
if (googleEvent.Organizer != null)
|
||||
{
|
||||
calendarEvent.OrganizerDisplayName = googleEvent.Organizer.DisplayName;
|
||||
calendarEvent.OrganizerEmail = googleEvent.Organizer.Email;
|
||||
}
|
||||
|
||||
// Handle timestamps
|
||||
if (googleEvent.CreatedDateTimeOffset.HasValue)
|
||||
{
|
||||
calendarEvent.CreatedDate = googleEvent.CreatedDateTimeOffset.Value.DateTime;
|
||||
}
|
||||
|
||||
if (googleEvent.UpdatedDateTimeOffset.HasValue)
|
||||
{
|
||||
calendarEvent.LastModified = googleEvent.UpdatedDateTimeOffset.Value.DateTime;
|
||||
}
|
||||
|
||||
// Handle original start time for recurring event instances
|
||||
if (googleEvent.OriginalStartTime != null)
|
||||
{
|
||||
if (googleEvent.OriginalStartTime.Date != null)
|
||||
{
|
||||
calendarEvent.OriginalStartTime = googleEvent.OriginalStartTime.Date;
|
||||
}
|
||||
else if (googleEvent.OriginalStartTime.DateTimeDateTimeOffset.HasValue)
|
||||
{
|
||||
calendarEvent.OriginalStartTime = googleEvent.OriginalStartTime.DateTimeDateTimeOffset.Value.ToString("O");
|
||||
}
|
||||
}
|
||||
|
||||
// Automatically determine the calendar item type based on event properties
|
||||
calendarEvent.DetermineItemType();
|
||||
|
||||
return calendarEvent;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// RRULE, EXRULE, RDATE and EXDATE lines for a recurring event, as specified in RFC5545.
|
||||
/// Converts a Google Calendar API response status string to AttendeeResponseStatus enum
|
||||
/// </summary>
|
||||
/// <returns>___ separated lines.</returns>
|
||||
public static string GetRecurrenceString(this Event calendarEvent)
|
||||
/// <param name="googleStatus">The status string from Google Calendar API</param>
|
||||
/// <returns>Corresponding AttendeeResponseStatus enum value</returns>
|
||||
public static AttendeeResponseStatus FromGoogleStatus(string? googleStatus)
|
||||
{
|
||||
if (calendarEvent == null || calendarEvent.Recurrence == null || !calendarEvent.Recurrence.Any())
|
||||
return googleStatus?.ToLowerInvariant() switch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
"accepted" => AttendeeResponseStatus.Accepted,
|
||||
"declined" => AttendeeResponseStatus.Declined,
|
||||
"tentative" => AttendeeResponseStatus.Tentative,
|
||||
"needsaction" => AttendeeResponseStatus.NeedsAction,
|
||||
_ => AttendeeResponseStatus.NeedsAction
|
||||
};
|
||||
}
|
||||
|
||||
return string.Join(Constants.CalendarEventRecurrenceRuleSeperator, calendarEvent.Recurrence);
|
||||
/// <summary>
|
||||
/// Converts an AttendeeResponseStatus enum to Google Calendar API response status string
|
||||
/// </summary>
|
||||
/// <param name="status">The AttendeeResponseStatus enum value</param>
|
||||
/// <returns>Corresponding Google Calendar API status string</returns>
|
||||
public static string ToGoogleStatus(AttendeeResponseStatus status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
AttendeeResponseStatus.Accepted => "accepted",
|
||||
AttendeeResponseStatus.Declined => "declined",
|
||||
AttendeeResponseStatus.Tentative => "tentative",
|
||||
AttendeeResponseStatus.NeedsAction => "needsAction",
|
||||
_ => "needsAction"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.Graph.Models;
|
||||
using MimeKit;
|
||||
using Wino.Core.Domain.Entities.Calendar;
|
||||
@@ -127,125 +126,26 @@ public static class OutlookIntegratorExtensions
|
||||
return calendar;
|
||||
}
|
||||
|
||||
private static string GetRfc5545DayOfWeek(DayOfWeekObject dayOfWeek)
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Converts Outlook response status to our enum
|
||||
/// </summary>
|
||||
/// <param name="outlookResponse">Outlook response type</param>
|
||||
/// <returns>AttendeeResponseStatus enum value</returns>
|
||||
public static AttendeeResponseStatus ConvertOutlookResponseStatus(this Microsoft.Graph.Models.ResponseType? outlookResponse)
|
||||
{
|
||||
return dayOfWeek switch
|
||||
return outlookResponse switch
|
||||
{
|
||||
DayOfWeekObject.Monday => "MO",
|
||||
DayOfWeekObject.Tuesday => "TU",
|
||||
DayOfWeekObject.Wednesday => "WE",
|
||||
DayOfWeekObject.Thursday => "TH",
|
||||
DayOfWeekObject.Friday => "FR",
|
||||
DayOfWeekObject.Saturday => "SA",
|
||||
DayOfWeekObject.Sunday => "SU",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(dayOfWeek), dayOfWeek, null)
|
||||
Microsoft.Graph.Models.ResponseType.Accepted => AttendeeResponseStatus.Accepted,
|
||||
Microsoft.Graph.Models.ResponseType.Declined => AttendeeResponseStatus.Declined,
|
||||
Microsoft.Graph.Models.ResponseType.TentativelyAccepted => AttendeeResponseStatus.Tentative,
|
||||
Microsoft.Graph.Models.ResponseType.None => AttendeeResponseStatus.NeedsAction,
|
||||
Microsoft.Graph.Models.ResponseType.NotResponded => AttendeeResponseStatus.NeedsAction,
|
||||
_ => AttendeeResponseStatus.NeedsAction
|
||||
};
|
||||
}
|
||||
|
||||
public static string ToRfc5545RecurrenceString(this PatternedRecurrence recurrence)
|
||||
{
|
||||
if (recurrence == null || recurrence.Pattern == null)
|
||||
throw new ArgumentNullException(nameof(recurrence), "PatternedRecurrence or its Pattern cannot be null.");
|
||||
|
||||
var ruleBuilder = new StringBuilder("RRULE:");
|
||||
var pattern = recurrence.Pattern;
|
||||
|
||||
// Frequency
|
||||
switch (pattern.Type)
|
||||
{
|
||||
case RecurrencePatternType.Daily:
|
||||
ruleBuilder.Append("FREQ=DAILY;");
|
||||
break;
|
||||
case RecurrencePatternType.Weekly:
|
||||
ruleBuilder.Append("FREQ=WEEKLY;");
|
||||
break;
|
||||
case RecurrencePatternType.AbsoluteMonthly:
|
||||
ruleBuilder.Append("FREQ=MONTHLY;");
|
||||
break;
|
||||
case RecurrencePatternType.AbsoluteYearly:
|
||||
ruleBuilder.Append("FREQ=YEARLY;");
|
||||
break;
|
||||
case RecurrencePatternType.RelativeMonthly:
|
||||
ruleBuilder.Append("FREQ=MONTHLY;");
|
||||
break;
|
||||
case RecurrencePatternType.RelativeYearly:
|
||||
ruleBuilder.Append("FREQ=YEARLY;");
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException($"Unsupported recurrence pattern type: {pattern.Type}");
|
||||
}
|
||||
|
||||
// Interval
|
||||
if (pattern.Interval > 0)
|
||||
ruleBuilder.Append($"INTERVAL={pattern.Interval};");
|
||||
|
||||
// Days of Week
|
||||
if (pattern.DaysOfWeek?.Any() == true)
|
||||
{
|
||||
var days = string.Join(",", pattern.DaysOfWeek.Select(day => day.ToString().ToUpperInvariant().Substring(0, 2)));
|
||||
ruleBuilder.Append($"BYDAY={days};");
|
||||
}
|
||||
|
||||
// Day of Month (BYMONTHDAY)
|
||||
if (pattern.Type == RecurrencePatternType.AbsoluteMonthly || pattern.Type == RecurrencePatternType.AbsoluteYearly)
|
||||
{
|
||||
if (pattern.DayOfMonth <= 0)
|
||||
throw new ArgumentException("DayOfMonth must be greater than 0 for absoluteMonthly or absoluteYearly patterns.");
|
||||
|
||||
ruleBuilder.Append($"BYMONTHDAY={pattern.DayOfMonth};");
|
||||
}
|
||||
|
||||
// Month (BYMONTH)
|
||||
if (pattern.Type == RecurrencePatternType.AbsoluteYearly || pattern.Type == RecurrencePatternType.RelativeYearly)
|
||||
{
|
||||
if (pattern.Month <= 0)
|
||||
throw new ArgumentException("Month must be greater than 0 for absoluteYearly or relativeYearly patterns.");
|
||||
|
||||
ruleBuilder.Append($"BYMONTH={pattern.Month};");
|
||||
}
|
||||
|
||||
// Count or Until
|
||||
if (recurrence.Range != null)
|
||||
{
|
||||
if (recurrence.Range.Type == RecurrenceRangeType.EndDate && recurrence.Range.EndDate != null)
|
||||
{
|
||||
ruleBuilder.Append($"UNTIL={recurrence.Range.EndDate.Value:yyyyMMddTHHmmssZ};");
|
||||
}
|
||||
else if (recurrence.Range.Type == RecurrenceRangeType.Numbered && recurrence.Range.NumberOfOccurrences.HasValue)
|
||||
{
|
||||
ruleBuilder.Append($"COUNT={recurrence.Range.NumberOfOccurrences.Value};");
|
||||
}
|
||||
}
|
||||
|
||||
// Remove trailing semicolon
|
||||
return ruleBuilder.ToString().TrimEnd(';');
|
||||
}
|
||||
|
||||
public static DateTimeOffset GetDateTimeOffsetFromDateTimeTimeZone(DateTimeTimeZone dateTimeTimeZone)
|
||||
{
|
||||
if (dateTimeTimeZone == null || string.IsNullOrEmpty(dateTimeTimeZone.DateTime) || string.IsNullOrEmpty(dateTimeTimeZone.TimeZone))
|
||||
{
|
||||
throw new ArgumentException("DateTimeTimeZone is null or empty.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Parse the DateTime string
|
||||
if (DateTime.TryParse(dateTimeTimeZone.DateTime, out DateTime parsedDateTime))
|
||||
{
|
||||
// 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 (Exception)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static AttendeeStatus GetAttendeeStatus(ResponseType? responseType)
|
||||
{
|
||||
@@ -261,24 +161,6 @@ public static class OutlookIntegratorExtensions
|
||||
};
|
||||
}
|
||||
|
||||
public static CalendarEventAttendee CreateAttendee(this Attendee attendee, Guid calendarItemId)
|
||||
{
|
||||
bool isOrganizer = attendee?.Status?.Response == ResponseType.Organizer;
|
||||
|
||||
var eventAttendee = new CalendarEventAttendee()
|
||||
{
|
||||
CalendarItemId = calendarItemId,
|
||||
Id = Guid.NewGuid(),
|
||||
Email = attendee.EmailAddress?.Address,
|
||||
Name = attendee.EmailAddress?.Name,
|
||||
AttendenceStatus = GetAttendeeStatus(attendee.Status.Response),
|
||||
IsOrganizer = isOrganizer,
|
||||
IsOptionalAttendee = attendee.Type == AttendeeType.Optional,
|
||||
};
|
||||
|
||||
return eventAttendee;
|
||||
}
|
||||
|
||||
#region Mime to Outlook Message Helpers
|
||||
|
||||
private static IEnumerable<Recipient> GetRecipients(this InternetAddressList internetAddresses)
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace Wino.Core.Integration.Processors;
|
||||
/// <see cref="IGmailChangeProcessor"/>, <see cref="IOutlookChangeProcessor"/> and <see cref="IImapChangeProcessor"/>
|
||||
/// None of the synchronizers can directly change anything in the database.
|
||||
/// </summary>
|
||||
public interface IDefaultChangeProcessor
|
||||
public interface IDefaultChangeProcessor : ICalendarServiceEx
|
||||
{
|
||||
Task UpdateAccountAsync(MailAccount account);
|
||||
// Task<string> UpdateAccountDeltaSynchronizationIdentifierAsync(Guid accountId, string deltaSynchronizationIdentifier);
|
||||
@@ -113,13 +113,14 @@ public class DefaultChangeProcessor(IDatabaseService databaseService,
|
||||
IMailService mailService,
|
||||
ICalendarService calendarService,
|
||||
IAccountService accountService,
|
||||
IMimeFileService mimeFileService) : BaseDatabaseService(databaseService), IDefaultChangeProcessor
|
||||
ICalendarServiceEx calendarServiceEx,
|
||||
IMimeFileService mimeFileService) : BaseDatabaseService(databaseService), IDefaultChangeProcessor, ICalendarServiceEx
|
||||
{
|
||||
protected IMailService MailService = mailService;
|
||||
protected ICalendarService CalendarService = calendarService;
|
||||
protected IFolderService FolderService = folderService;
|
||||
protected IAccountService AccountService = accountService;
|
||||
|
||||
private readonly ICalendarServiceEx _calendarServiceEx = calendarServiceEx;
|
||||
private readonly IMimeFileService _mimeFileService = mimeFileService;
|
||||
|
||||
public Task<string> UpdateAccountDeltaSynchronizationIdentifierAsync(Guid accountId, string synchronizationDeltaIdentifier)
|
||||
@@ -208,4 +209,234 @@ public class DefaultChangeProcessor(IDatabaseService databaseService,
|
||||
|
||||
public Task<bool> IsMailExistsInFolderAsync(string messageId, Guid folderId)
|
||||
=> MailService.IsMailExistsAsync(messageId, folderId);
|
||||
|
||||
// TODO: Normalize this shit. Not everything needs to be exposed for processor.
|
||||
#region ICalendarServiceEx
|
||||
|
||||
public Task<int> ClearAllCalendarEventAttendeesAsync()
|
||||
{
|
||||
return _calendarServiceEx.ClearAllCalendarEventAttendeesAsync();
|
||||
}
|
||||
|
||||
public Task<int> ClearAllCalendarsAsync()
|
||||
{
|
||||
return _calendarServiceEx.ClearAllCalendarsAsync();
|
||||
}
|
||||
|
||||
public Task<int> ClearAllDataAsync()
|
||||
{
|
||||
return _calendarServiceEx.ClearAllDataAsync();
|
||||
}
|
||||
|
||||
public Task<int> ClearAllEventsAsync()
|
||||
{
|
||||
return _calendarServiceEx.ClearAllEventsAsync();
|
||||
}
|
||||
|
||||
public Task<int> DeleteCalendarAsync(string remoteCalendarId)
|
||||
{
|
||||
return _calendarServiceEx.DeleteCalendarAsync(remoteCalendarId);
|
||||
}
|
||||
|
||||
public Task<int> DeleteCalendarEventAttendeesForEventAsync(Guid eventId)
|
||||
{
|
||||
return _calendarServiceEx.DeleteCalendarEventAttendeesForEventAsync(eventId);
|
||||
}
|
||||
|
||||
public Task<int> DeleteEventAsync(string remoteEventId)
|
||||
{
|
||||
return _calendarServiceEx.DeleteEventAsync(remoteEventId);
|
||||
}
|
||||
|
||||
public Task<List<CalendarEventAttendee>> GetAllCalendarEventAttendeesAsync()
|
||||
{
|
||||
return _calendarServiceEx.GetAllCalendarEventAttendeesAsync();
|
||||
}
|
||||
|
||||
public Task<List<AccountCalendar>> GetAllCalendarsAsync()
|
||||
{
|
||||
return _calendarServiceEx.GetAllCalendarsAsync();
|
||||
}
|
||||
|
||||
public Task<List<CalendarItem>> GetAllDayEventsAsync()
|
||||
{
|
||||
return _calendarServiceEx.GetAllDayEventsAsync();
|
||||
}
|
||||
|
||||
public Task<List<CalendarItem>> GetAllEventsAsync()
|
||||
{
|
||||
return _calendarServiceEx.GetAllEventsAsync();
|
||||
}
|
||||
|
||||
public Task<List<CalendarItem>> GetAllEventsIncludingDeletedAsync()
|
||||
{
|
||||
return _calendarServiceEx.GetAllEventsIncludingDeletedAsync();
|
||||
}
|
||||
|
||||
public Task<List<CalendarItem>> GetAllRecurringEventsByTypeAsync()
|
||||
{
|
||||
return _calendarServiceEx.GetAllRecurringEventsByTypeAsync();
|
||||
}
|
||||
|
||||
public Task<AccountCalendar> GetCalendarByRemoteIdAsync(string remoteCalendarId)
|
||||
{
|
||||
return _calendarServiceEx.GetCalendarByRemoteIdAsync(remoteCalendarId);
|
||||
}
|
||||
|
||||
public Task<Dictionary<AttendeeResponseStatus, int>> GetCalendarEventAttendeeResponseCountsAsync(Guid eventId)
|
||||
{
|
||||
return _calendarServiceEx.GetCalendarEventAttendeeResponseCountsAsync(eventId);
|
||||
}
|
||||
|
||||
public Task<List<CalendarEventAttendee>> GetCalendarEventAttendeesForEventAsync(Guid eventId)
|
||||
{
|
||||
return _calendarServiceEx.GetCalendarEventAttendeesForEventAsync(eventId);
|
||||
}
|
||||
|
||||
public Task<List<CalendarEventAttendee>> GetCalendarEventAttendeesForEventByRemoteIdAsync(string remoteEventId)
|
||||
{
|
||||
return _calendarServiceEx.GetCalendarEventAttendeesForEventByRemoteIdAsync(remoteEventId);
|
||||
}
|
||||
|
||||
public Task<string> GetCalendarSyncTokenAsync(string calendarId)
|
||||
{
|
||||
return _calendarServiceEx.GetCalendarSyncTokenAsync(calendarId);
|
||||
}
|
||||
|
||||
public Task<CalendarItem> GetEventByRemoteIdAsync(string remoteEventId)
|
||||
{
|
||||
return _calendarServiceEx.GetEventByRemoteIdAsync(remoteEventId);
|
||||
}
|
||||
|
||||
public Task<List<CalendarItem>> GetEventsByItemTypeAsync(CalendarItemType itemType)
|
||||
{
|
||||
return _calendarServiceEx.GetEventsByItemTypeAsync(itemType);
|
||||
}
|
||||
|
||||
public Task<List<CalendarItem>> GetEventsByItemTypesAsync(params CalendarItemType[] itemTypes)
|
||||
{
|
||||
return _calendarServiceEx.GetEventsByItemTypesAsync(itemTypes);
|
||||
}
|
||||
|
||||
public Task<List<CalendarItem>> GetEventsByremoteCalendarIdAsync(string remoteCalendarId)
|
||||
{
|
||||
return _calendarServiceEx.GetEventsByremoteCalendarIdAsync(remoteCalendarId);
|
||||
}
|
||||
|
||||
public Task<List<CalendarItem>> GetEventsForCalendarAsync(Guid calendarId)
|
||||
{
|
||||
return _calendarServiceEx.GetEventsForCalendarAsync(calendarId);
|
||||
}
|
||||
|
||||
public Task<List<CalendarItem>> GetEventsInDateRangeAsync(DateTime startDate, DateTime endDate)
|
||||
{
|
||||
return _calendarServiceEx.GetEventsInDateRangeAsync(startDate, endDate);
|
||||
}
|
||||
|
||||
public Task<List<CalendarItem>> GetEventsSinceLastSyncAsync(DateTime? lastSyncTime)
|
||||
{
|
||||
return _calendarServiceEx.GetEventsSinceLastSyncAsync(lastSyncTime);
|
||||
}
|
||||
|
||||
public Task<Dictionary<CalendarItemType, int>> GetEventStatsByItemTypeAsync()
|
||||
{
|
||||
return _calendarServiceEx.GetEventStatsByItemTypeAsync();
|
||||
}
|
||||
|
||||
public Task<List<CalendarItem>> GetExpandedEventsInDateRangeAsync(DateTime startDate, DateTime endDate)
|
||||
{
|
||||
return _calendarServiceEx.GetExpandedEventsInDateRangeAsync(startDate, endDate);
|
||||
}
|
||||
|
||||
public Task<List<CalendarItem>> GetExpandedEventsInDateRangeWithExceptionsAsync(DateTime startDate, DateTime endDate, AccountCalendar calendar)
|
||||
{
|
||||
return _calendarServiceEx.GetExpandedEventsInDateRangeWithExceptionsAsync(startDate, endDate, calendar);
|
||||
}
|
||||
|
||||
public Task<DateTime?> GetLastSyncTimeAsync(string calendarId)
|
||||
{
|
||||
return _calendarServiceEx.GetLastSyncTimeAsync(calendarId);
|
||||
}
|
||||
|
||||
public Task<List<CalendarItem>> GetMultiDayEventsAsync()
|
||||
{
|
||||
return _calendarServiceEx.GetMultiDayEventsAsync();
|
||||
}
|
||||
|
||||
public Task<List<CalendarItem>> GetRecurringEventsAsync()
|
||||
{
|
||||
return _calendarServiceEx.GetRecurringEventsAsync();
|
||||
}
|
||||
|
||||
public Task<int> HardDeleteEventAsync(string remoteEventId)
|
||||
{
|
||||
return _calendarServiceEx.HardDeleteEventAsync(remoteEventId);
|
||||
}
|
||||
|
||||
public Task<int> InsertCalendarAsync(AccountCalendar calendar)
|
||||
{
|
||||
return _calendarServiceEx.InsertCalendarAsync(calendar);
|
||||
}
|
||||
|
||||
public Task<int> InsertCalendarEventAttendeeAsync(CalendarEventAttendee calendareventattendee)
|
||||
{
|
||||
return _calendarServiceEx.InsertCalendarEventAttendeeAsync(calendareventattendee);
|
||||
}
|
||||
|
||||
public Task<int> InsertEventAsync(CalendarItem calendarItem)
|
||||
{
|
||||
return _calendarServiceEx.InsertEventAsync(calendarItem);
|
||||
}
|
||||
|
||||
public Task<int> MarkEventAsDeletedAsync(string remoteEventId, string remoteCalendarId)
|
||||
{
|
||||
return _calendarServiceEx.MarkEventAsDeletedAsync(remoteEventId, remoteCalendarId);
|
||||
}
|
||||
|
||||
public Task<int> SyncCalendarEventAttendeesForEventAsync(Guid eventId, List<CalendarEventAttendee> calendareventattendees)
|
||||
{
|
||||
return _calendarServiceEx.SyncCalendarEventAttendeesForEventAsync(eventId, calendareventattendees);
|
||||
}
|
||||
|
||||
public Task<int> UpdateAllEventItemTypesAsync()
|
||||
{
|
||||
return _calendarServiceEx.UpdateAllEventItemTypesAsync();
|
||||
}
|
||||
|
||||
public Task<int> UpdateCalendarAsync(AccountCalendar calendar)
|
||||
{
|
||||
return _calendarServiceEx.UpdateCalendarAsync(calendar);
|
||||
}
|
||||
|
||||
public Task<int> UpdateCalendarEventAttendeeAsync(CalendarEventAttendee calendareventattendee)
|
||||
{
|
||||
return _calendarServiceEx.UpdateCalendarEventAttendeeAsync(calendareventattendee);
|
||||
}
|
||||
|
||||
public Task<int> UpdateCalendarSyncTokenAsync(string calendarId, string syncToken)
|
||||
{
|
||||
return _calendarServiceEx.UpdateCalendarSyncTokenAsync(calendarId, syncToken);
|
||||
}
|
||||
|
||||
public Task<int> UpdateEventAsync(CalendarItem calendarItem)
|
||||
{
|
||||
return _calendarServiceEx.UpdateEventAsync(calendarItem);
|
||||
}
|
||||
|
||||
public Task<int> UpsertCalendarAsync(AccountCalendar calendar)
|
||||
{
|
||||
return _calendarServiceEx.UpsertCalendarAsync(calendar);
|
||||
}
|
||||
|
||||
public Task<int> UpsertEventAsync(CalendarItem calendarItem)
|
||||
{
|
||||
return _calendarServiceEx.UpsertEventAsync(calendarItem);
|
||||
}
|
||||
|
||||
public Task<int> SyncAttendeesForEventAsync(Guid eventId, List<CalendarEventAttendee> attendees)
|
||||
{
|
||||
return _calendarServiceEx.SyncAttendeesForEventAsync(eventId, attendees);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Tasks;
|
||||
using Google.Apis.Calendar.v3.Data;
|
||||
using Serilog;
|
||||
using Wino.Core.Domain.Entities.Calendar;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Core.Extensions;
|
||||
using Wino.Services;
|
||||
using CalendarEventAttendee = Wino.Core.Domain.Entities.Calendar.CalendarEventAttendee;
|
||||
using CalendarItem = Wino.Core.Domain.Entities.Calendar.CalendarItem;
|
||||
|
||||
namespace Wino.Core.Integration.Processors;
|
||||
|
||||
@@ -23,7 +18,8 @@ public class GmailChangeProcessor : DefaultChangeProcessor, IGmailChangeProcesso
|
||||
IMailService mailService,
|
||||
ICalendarService calendarService,
|
||||
IAccountService accountService,
|
||||
IMimeFileService mimeFileService) : base(databaseService, folderService, mailService, calendarService, accountService, mimeFileService)
|
||||
ICalendarServiceEx calendarServiceEx,
|
||||
IMimeFileService mimeFileService) : base(databaseService, folderService, mailService, calendarService, accountService, calendarServiceEx, mimeFileService)
|
||||
{
|
||||
|
||||
}
|
||||
@@ -36,224 +32,7 @@ public class GmailChangeProcessor : DefaultChangeProcessor, IGmailChangeProcesso
|
||||
|
||||
public async Task ManageCalendarEventAsync(Event calendarEvent, AccountCalendar assignedCalendar, MailAccount organizerAccount)
|
||||
{
|
||||
var status = calendarEvent.Status;
|
||||
|
||||
var recurringEventId = calendarEvent.RecurringEventId;
|
||||
|
||||
// 1. Canceled exceptions of recurred events are only guaranteed to have recurringEventId, Id and start time.
|
||||
// 2. Updated exceptions of recurred events have different Id, but recurringEventId is the same as parent.
|
||||
|
||||
// Check if we have this event before.
|
||||
var existingCalendarItem = await CalendarService.GetCalendarItemAsync(assignedCalendar.Id, calendarEvent.Id);
|
||||
|
||||
if (existingCalendarItem == null)
|
||||
{
|
||||
CalendarItem parentRecurringEvent = null;
|
||||
|
||||
// Manage the recurring event id.
|
||||
if (!string.IsNullOrEmpty(recurringEventId))
|
||||
{
|
||||
parentRecurringEvent = await CalendarService.GetCalendarItemAsync(assignedCalendar.Id, recurringEventId).ConfigureAwait(false);
|
||||
|
||||
if (parentRecurringEvent == null)
|
||||
{
|
||||
Log.Information($"Parent recurring event is missing for event. Skipping creation of {calendarEvent.Id}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// We don't have this event yet. Create a new one.
|
||||
var eventStartDateTimeOffset = GoogleIntegratorExtensions.GetEventDateTimeOffset(calendarEvent.Start);
|
||||
var eventEndDateTimeOffset = GoogleIntegratorExtensions.GetEventDateTimeOffset(calendarEvent.End);
|
||||
|
||||
double totalDurationInSeconds = 0;
|
||||
|
||||
if (eventStartDateTimeOffset != null && eventEndDateTimeOffset != null)
|
||||
{
|
||||
totalDurationInSeconds = (eventEndDateTimeOffset.Value - eventStartDateTimeOffset.Value).TotalSeconds;
|
||||
}
|
||||
|
||||
CalendarItem calendarItem = null;
|
||||
|
||||
if (parentRecurringEvent != null)
|
||||
{
|
||||
// Exceptions of parent events might not have all the fields populated.
|
||||
// We must use the parent event's data for fields that don't exists.
|
||||
|
||||
// Update duration if it's not populated.
|
||||
if (totalDurationInSeconds == 0)
|
||||
{
|
||||
totalDurationInSeconds = parentRecurringEvent.DurationInSeconds;
|
||||
}
|
||||
|
||||
var organizerMail = GetOrganizerEmail(calendarEvent, organizerAccount);
|
||||
var organizerName = GetOrganizerName(calendarEvent, organizerAccount);
|
||||
|
||||
|
||||
calendarItem = new CalendarItem()
|
||||
{
|
||||
CalendarId = assignedCalendar.Id,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
Description = calendarEvent.Description ?? parentRecurringEvent.Description,
|
||||
Id = Guid.NewGuid(),
|
||||
StartDate = eventStartDateTimeOffset.Value.DateTime,
|
||||
StartDateOffset = eventStartDateTimeOffset.Value.Offset,
|
||||
EndDateOffset = eventEndDateTimeOffset?.Offset ?? parentRecurringEvent.EndDateOffset,
|
||||
DurationInSeconds = totalDurationInSeconds,
|
||||
Location = string.IsNullOrEmpty(calendarEvent.Location) ? parentRecurringEvent.Location : calendarEvent.Location,
|
||||
|
||||
// Leave it empty if it's not populated.
|
||||
Recurrence = GoogleIntegratorExtensions.GetRecurrenceString(calendarEvent) == null ? string.Empty : GoogleIntegratorExtensions.GetRecurrenceString(calendarEvent),
|
||||
Status = GetStatus(calendarEvent.Status),
|
||||
Title = string.IsNullOrEmpty(calendarEvent.Summary) ? parentRecurringEvent.Title : calendarEvent.Summary,
|
||||
UpdatedAt = DateTimeOffset.UtcNow,
|
||||
Visibility = string.IsNullOrEmpty(calendarEvent.Visibility) ? parentRecurringEvent.Visibility : GetVisibility(calendarEvent.Visibility),
|
||||
HtmlLink = string.IsNullOrEmpty(calendarEvent.HtmlLink) ? parentRecurringEvent.HtmlLink : calendarEvent.HtmlLink,
|
||||
RemoteEventId = calendarEvent.Id,
|
||||
IsLocked = calendarEvent.Locked.GetValueOrDefault(),
|
||||
OrganizerDisplayName = string.IsNullOrEmpty(organizerName) ? parentRecurringEvent.OrganizerDisplayName : organizerName,
|
||||
OrganizerEmail = string.IsNullOrEmpty(organizerMail) ? parentRecurringEvent.OrganizerEmail : organizerMail
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
// This is a parent event creation.
|
||||
// Start-End dates are guaranteed to be populated.
|
||||
|
||||
if (eventStartDateTimeOffset == null || eventEndDateTimeOffset == null)
|
||||
{
|
||||
Log.Error("Failed to create parent event because either start or end date is not specified.");
|
||||
return;
|
||||
}
|
||||
|
||||
calendarItem = new CalendarItem()
|
||||
{
|
||||
CalendarId = assignedCalendar.Id,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
Description = calendarEvent.Description,
|
||||
Id = Guid.NewGuid(),
|
||||
StartDate = eventStartDateTimeOffset.Value.DateTime,
|
||||
StartDateOffset = eventStartDateTimeOffset.Value.Offset,
|
||||
EndDateOffset = eventEndDateTimeOffset.Value.Offset,
|
||||
DurationInSeconds = totalDurationInSeconds,
|
||||
Location = calendarEvent.Location,
|
||||
Recurrence = GoogleIntegratorExtensions.GetRecurrenceString(calendarEvent),
|
||||
Status = GetStatus(calendarEvent.Status),
|
||||
Title = calendarEvent.Summary,
|
||||
UpdatedAt = DateTimeOffset.UtcNow,
|
||||
Visibility = GetVisibility(calendarEvent.Visibility),
|
||||
HtmlLink = calendarEvent.HtmlLink,
|
||||
RemoteEventId = calendarEvent.Id,
|
||||
IsLocked = calendarEvent.Locked.GetValueOrDefault(),
|
||||
OrganizerDisplayName = GetOrganizerName(calendarEvent, organizerAccount),
|
||||
OrganizerEmail = GetOrganizerEmail(calendarEvent, organizerAccount)
|
||||
};
|
||||
}
|
||||
|
||||
// Hide canceled events.
|
||||
calendarItem.IsHidden = calendarItem.Status == CalendarItemStatus.Cancelled;
|
||||
|
||||
// Manage the recurring event id.
|
||||
if (parentRecurringEvent != null)
|
||||
{
|
||||
calendarItem.RecurringCalendarItemId = parentRecurringEvent.Id;
|
||||
}
|
||||
|
||||
Debug.WriteLine($"({assignedCalendar.Name}) {calendarItem.Title}, Start: {calendarItem.StartDate.ToString("f")}, End: {calendarItem.EndDate.ToString("f")}");
|
||||
|
||||
// Attendees
|
||||
var attendees = new List<CalendarEventAttendee>();
|
||||
|
||||
if (calendarEvent.Attendees == null)
|
||||
{
|
||||
// Self-only event.
|
||||
|
||||
attendees.Add(new CalendarEventAttendee()
|
||||
{
|
||||
CalendarItemId = calendarItem.Id,
|
||||
IsOrganizer = true,
|
||||
Email = organizerAccount.Address,
|
||||
Name = organizerAccount.SenderName,
|
||||
AttendenceStatus = AttendeeStatus.Accepted,
|
||||
Id = Guid.NewGuid(),
|
||||
IsOptionalAttendee = false,
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var attendee in calendarEvent.Attendees)
|
||||
{
|
||||
if (attendee.Self == true)
|
||||
{
|
||||
// TODO:
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(attendee.Email))
|
||||
{
|
||||
AttendeeStatus GetAttendenceStatus(string responseStatus)
|
||||
{
|
||||
return responseStatus switch
|
||||
{
|
||||
"accepted" => AttendeeStatus.Accepted,
|
||||
"declined" => AttendeeStatus.Declined,
|
||||
"tentative" => AttendeeStatus.Tentative,
|
||||
"needsAction" => AttendeeStatus.NeedsAction,
|
||||
_ => AttendeeStatus.NeedsAction
|
||||
};
|
||||
}
|
||||
|
||||
var eventAttendee = new CalendarEventAttendee()
|
||||
{
|
||||
CalendarItemId = calendarItem.Id,
|
||||
IsOrganizer = attendee.Organizer ?? false,
|
||||
Comment = attendee.Comment,
|
||||
Email = attendee.Email,
|
||||
Name = attendee.DisplayName,
|
||||
AttendenceStatus = GetAttendenceStatus(attendee.ResponseStatus),
|
||||
Id = Guid.NewGuid(),
|
||||
IsOptionalAttendee = attendee.Optional ?? false,
|
||||
};
|
||||
|
||||
attendees.Add(eventAttendee);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await CalendarService.CreateNewCalendarItemAsync(calendarItem, attendees);
|
||||
}
|
||||
else
|
||||
{
|
||||
// We have this event already. Update it.
|
||||
if (calendarEvent.Status == "cancelled")
|
||||
{
|
||||
// Parent event is canceled. We must delete everything.
|
||||
if (string.IsNullOrEmpty(recurringEventId))
|
||||
{
|
||||
Log.Information("Parent event is canceled. Deleting all instances of {Id}", existingCalendarItem.Id);
|
||||
|
||||
await CalendarService.DeleteCalendarItemAsync(existingCalendarItem.Id).ConfigureAwait(false);
|
||||
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Child event is canceled.
|
||||
// Child should live as long as parent lives, but must not be displayed to the user.
|
||||
|
||||
existingCalendarItem.IsHidden = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Make sure to unhide the event.
|
||||
// It might be marked as hidden before.
|
||||
existingCalendarItem.IsHidden = false;
|
||||
|
||||
// Update the event properties.
|
||||
}
|
||||
}
|
||||
|
||||
// Upsert the event.
|
||||
await Connection.InsertOrReplaceAsync(existingCalendarItem);
|
||||
// TODO:
|
||||
}
|
||||
|
||||
private string GetOrganizerName(Event calendarEvent, MailAccount account)
|
||||
|
||||
@@ -13,7 +13,8 @@ public class ImapChangeProcessor : DefaultChangeProcessor, IImapChangeProcessor
|
||||
IMailService mailService,
|
||||
IAccountService accountService,
|
||||
ICalendarService calendarService,
|
||||
IMimeFileService mimeFileService) : base(databaseService, folderService, mailService, calendarService, accountService, mimeFileService)
|
||||
ICalendarServiceEx calendarServiceEx,
|
||||
IMimeFileService mimeFileService) : base(databaseService, folderService, mailService, calendarService, accountService, calendarServiceEx, mimeFileService)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Graph.Models;
|
||||
using Serilog;
|
||||
using Wino.Core.Domain.Entities.Calendar;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Extensions;
|
||||
using Wino.Services;
|
||||
|
||||
namespace Wino.Core.Integration.Processors;
|
||||
@@ -17,7 +13,8 @@ public class OutlookChangeProcessor(IDatabaseService databaseService,
|
||||
ICalendarService calendarService,
|
||||
IMailService mailService,
|
||||
IAccountService accountService,
|
||||
IMimeFileService mimeFileService) : DefaultChangeProcessor(databaseService, folderService, mailService, calendarService, accountService, mimeFileService)
|
||||
ICalendarServiceEx calendarServiceEx,
|
||||
IMimeFileService mimeFileService) : DefaultChangeProcessor(databaseService, folderService, mailService, calendarService, accountService, calendarServiceEx, mimeFileService)
|
||||
, IOutlookChangeProcessor
|
||||
{
|
||||
|
||||
@@ -40,106 +37,6 @@ public class OutlookChangeProcessor(IDatabaseService databaseService,
|
||||
|
||||
public async Task ManageCalendarEventAsync(Event calendarEvent, AccountCalendar assignedCalendar, MailAccount organizerAccount)
|
||||
{
|
||||
// We parse the occurrences based on the parent event.
|
||||
// There is literally no point to store them because
|
||||
// type=Exception events are the exceptional childs of recurrency parent event.
|
||||
|
||||
if (calendarEvent.Type == EventType.Occurrence) return;
|
||||
|
||||
var savingItem = await CalendarService.GetCalendarItemAsync(assignedCalendar.Id, calendarEvent.Id);
|
||||
|
||||
Guid savingItemId = Guid.Empty;
|
||||
|
||||
if (savingItem != null)
|
||||
savingItemId = savingItem.Id;
|
||||
else
|
||||
{
|
||||
savingItemId = Guid.NewGuid();
|
||||
savingItem = new CalendarItem() { Id = savingItemId };
|
||||
}
|
||||
|
||||
DateTimeOffset eventStartDateTimeOffset = OutlookIntegratorExtensions.GetDateTimeOffsetFromDateTimeTimeZone(calendarEvent.Start);
|
||||
DateTimeOffset eventEndDateTimeOffset = OutlookIntegratorExtensions.GetDateTimeOffsetFromDateTimeTimeZone(calendarEvent.End);
|
||||
|
||||
var durationInSeconds = (eventEndDateTimeOffset - eventStartDateTimeOffset).TotalSeconds;
|
||||
|
||||
savingItem.RemoteEventId = calendarEvent.Id;
|
||||
savingItem.StartDate = eventStartDateTimeOffset.DateTime;
|
||||
savingItem.StartDateOffset = eventStartDateTimeOffset.Offset;
|
||||
savingItem.EndDateOffset = eventEndDateTimeOffset.Offset;
|
||||
savingItem.DurationInSeconds = durationInSeconds;
|
||||
|
||||
savingItem.Title = calendarEvent.Subject;
|
||||
savingItem.Description = calendarEvent.Body?.Content;
|
||||
savingItem.Location = calendarEvent.Location?.DisplayName;
|
||||
|
||||
if (calendarEvent.Type == EventType.Exception && !string.IsNullOrEmpty(calendarEvent.SeriesMasterId))
|
||||
{
|
||||
// This is a recurring event exception.
|
||||
// We need to find the parent event and set it as recurring event id.
|
||||
|
||||
var parentEvent = await CalendarService.GetCalendarItemAsync(assignedCalendar.Id, calendarEvent.SeriesMasterId);
|
||||
|
||||
if (parentEvent != null)
|
||||
{
|
||||
savingItem.RecurringCalendarItemId = parentEvent.Id;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Warning($"Parent recurring event is missing for event. Skipping creation of {calendarEvent.Id}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert the recurrence pattern to string for parent recurring events.
|
||||
if (calendarEvent.Type == EventType.SeriesMaster && calendarEvent.Recurrence != null)
|
||||
{
|
||||
savingItem.Recurrence = OutlookIntegratorExtensions.ToRfc5545RecurrenceString(calendarEvent.Recurrence);
|
||||
}
|
||||
|
||||
savingItem.HtmlLink = calendarEvent.WebLink;
|
||||
savingItem.CalendarId = assignedCalendar.Id;
|
||||
savingItem.OrganizerEmail = calendarEvent.Organizer?.EmailAddress?.Address;
|
||||
savingItem.OrganizerDisplayName = calendarEvent.Organizer?.EmailAddress?.Name;
|
||||
savingItem.IsHidden = false;
|
||||
|
||||
if (calendarEvent.ResponseStatus?.Response != null)
|
||||
{
|
||||
switch (calendarEvent.ResponseStatus.Response.Value)
|
||||
{
|
||||
case ResponseType.None:
|
||||
case ResponseType.NotResponded:
|
||||
savingItem.Status = CalendarItemStatus.NotResponded;
|
||||
break;
|
||||
case ResponseType.TentativelyAccepted:
|
||||
savingItem.Status = CalendarItemStatus.Tentative;
|
||||
break;
|
||||
case ResponseType.Accepted:
|
||||
case ResponseType.Organizer:
|
||||
savingItem.Status = CalendarItemStatus.Confirmed;
|
||||
break;
|
||||
case ResponseType.Declined:
|
||||
savingItem.Status = CalendarItemStatus.Cancelled;
|
||||
savingItem.IsHidden = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
savingItem.Status = CalendarItemStatus.Confirmed;
|
||||
}
|
||||
|
||||
// Upsert the event.
|
||||
await Connection.InsertOrReplaceAsync(savingItem);
|
||||
|
||||
// Manage attendees.
|
||||
if (calendarEvent.Attendees != null)
|
||||
{
|
||||
// Clear all attendees for this event.
|
||||
var attendees = calendarEvent.Attendees.Select(a => a.CreateAttendee(savingItemId)).ToList();
|
||||
await CalendarService.ManageEventAttendeesAsync(savingItemId, attendees).ConfigureAwait(false);
|
||||
}
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,75 +361,207 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
||||
|
||||
await SynchronizeCalendarsAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
bool isInitialSync = string.IsNullOrEmpty(Account.SynchronizationDeltaIdentifier);
|
||||
|
||||
_logger.Debug("Is initial synchronization: {IsInitialSync}", isInitialSync);
|
||||
|
||||
var localCalendars = await _gmailChangeProcessor.GetAccountCalendarsAsync(Account.Id).ConfigureAwait(false);
|
||||
|
||||
// TODO: Better logging and exception handling.
|
||||
foreach (var calendar in localCalendars)
|
||||
{
|
||||
var request = _calendarService.Events.List(calendar.RemoteCalendarId);
|
||||
// We can do just delta sync. It will fallback to full sync if there are no sync tokens or if the token is expired.
|
||||
await DeltaSynchronizeCalendarAsync(calendar).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
request.SingleEvents = false;
|
||||
request.ShowDeleted = true;
|
||||
// TODO: Return proper result from delta or full sync.
|
||||
return new CalendarSynchronizationResult()
|
||||
{
|
||||
CompletedState = SynchronizationCompletedState.Success,
|
||||
DownloadedEvents = new List<ICalendarItem>(),
|
||||
};
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(calendar.SynchronizationDeltaToken))
|
||||
private async Task FullSynchronizeCalendarAsync(AccountCalendar calendar)
|
||||
{
|
||||
var calendarId = calendar.RemoteCalendarId;
|
||||
|
||||
try
|
||||
{
|
||||
// Get events from the last 30 days to 1 year in the future
|
||||
var timeMin = DateTime.Now.AddYears(-3);
|
||||
var timeMax = DateTime.Now.AddYears(2);
|
||||
|
||||
var request = _calendarService.Events.List(calendarId);
|
||||
request.TimeMinDateTimeOffset = timeMin;
|
||||
request.TimeMaxDateTimeOffset = timeMax;
|
||||
request.SingleEvents = false; // Include recurring events
|
||||
request.ShowDeleted = true; // Include deleted events for synchronization
|
||||
request.MaxResults = 2500; // Maximum allowed by Google Calendar API
|
||||
|
||||
var events = await request.ExecuteAsync();
|
||||
|
||||
if (events.Items != null && events.Items.Count > 0)
|
||||
{
|
||||
// If a sync token is available, perform an incremental sync
|
||||
request.SyncToken = calendar.SynchronizationDeltaToken;
|
||||
Console.WriteLine($"Processing {events.Items.Count} events from calendar: {calendarId}");
|
||||
|
||||
foreach (var googleEvent in events.Items)
|
||||
{
|
||||
await ProcessGoogleEventAsync(googleEvent, calendar);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// If no sync token, perform an initial sync
|
||||
// Fetch events from the past year
|
||||
|
||||
request.TimeMinDateTimeOffset = DateTimeOffset.UtcNow.AddYears(-1);
|
||||
Console.WriteLine($"No events found in calendar: {calendarId}");
|
||||
}
|
||||
|
||||
string nextPageToken;
|
||||
string syncToken;
|
||||
|
||||
var allEvents = new List<Event>();
|
||||
|
||||
do
|
||||
// Store the sync token for future delta syncs
|
||||
if (!string.IsNullOrEmpty(events.NextSyncToken))
|
||||
{
|
||||
// Execute the request
|
||||
var events = await request.ExecuteAsync();
|
||||
await _gmailChangeProcessor.UpdateCalendarDeltaSynchronizationToken(calendar.Id, events.NextSyncToken).ConfigureAwait(false);
|
||||
|
||||
// Process the fetched events
|
||||
if (events.Items != null)
|
||||
{
|
||||
allEvents.AddRange(events.Items);
|
||||
}
|
||||
|
||||
// Get the next page token and sync token
|
||||
nextPageToken = events.NextPageToken;
|
||||
syncToken = events.NextSyncToken;
|
||||
|
||||
// Set the next page token for subsequent requests
|
||||
request.PageToken = nextPageToken;
|
||||
|
||||
} while (!string.IsNullOrEmpty(nextPageToken));
|
||||
|
||||
calendar.SynchronizationDeltaToken = syncToken;
|
||||
|
||||
// allEvents contains new or updated events.
|
||||
// Process them and create/update local calendar items.
|
||||
|
||||
foreach (var @event in allEvents)
|
||||
{
|
||||
// TODO: Exception handling for event processing.
|
||||
// TODO: Also update attendees and other properties.
|
||||
|
||||
await _gmailChangeProcessor.ManageCalendarEventAsync(@event, calendar, Account).ConfigureAwait(false);
|
||||
Console.WriteLine($"Stored sync token for calendar {calendarId} to enable delta sync");
|
||||
}
|
||||
|
||||
await _gmailChangeProcessor.UpdateAccountCalendarAsync(calendar).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to synchronize calendar {calendarId}: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
private async Task<bool> DeltaSynchronizeCalendarAsync(AccountCalendar calendar)
|
||||
{
|
||||
var calendarId = calendar.RemoteCalendarId;
|
||||
|
||||
try
|
||||
{
|
||||
Console.WriteLine($"Starting delta sync for calendar: {calendarId}");
|
||||
|
||||
// Get the stored sync token for this calendar
|
||||
var syncToken = calendar.SynchronizationDeltaToken;
|
||||
|
||||
if (string.IsNullOrEmpty(syncToken))
|
||||
{
|
||||
Console.WriteLine($"No sync token found for calendar {calendarId}. Performing full sync...");
|
||||
await FullSynchronizeCalendarAsync(calendar);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Create the events list request with sync token
|
||||
var request = _calendarService.Events.List(calendarId);
|
||||
request.SyncToken = syncToken;
|
||||
request.ShowDeleted = true; // Important: include deleted events for delta sync
|
||||
request.SingleEvents = false; // Include recurring events
|
||||
|
||||
Console.WriteLine($"Requesting delta changes with sync token: {syncToken.Substring(0, Math.Min(20, syncToken.Length))}...");
|
||||
|
||||
Events events;
|
||||
try
|
||||
{
|
||||
events = await request.ExecuteAsync();
|
||||
}
|
||||
catch (Google.GoogleApiException ex) when (ex.HttpStatusCode == System.Net.HttpStatusCode.Gone)
|
||||
{
|
||||
// Sync token has expired, need to do full sync
|
||||
Console.WriteLine($"Sync token expired for calendar {calendarId}. Performing full sync...");
|
||||
await FullSynchronizeCalendarAsync(calendar);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (events.Items != null && events.Items.Count > 0)
|
||||
{
|
||||
Console.WriteLine($"Processing {events.Items.Count} delta changes for calendar: {calendarId}");
|
||||
|
||||
foreach (var googleEvent in events.Items)
|
||||
{
|
||||
await ProcessDeltaCalendarEventAsync(googleEvent, calendar);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"No changes found for calendar: {calendarId}");
|
||||
}
|
||||
|
||||
// Store the new sync token
|
||||
if (!string.IsNullOrEmpty(events.NextSyncToken))
|
||||
{
|
||||
await _gmailChangeProcessor.UpdateCalendarSyncTokenAsync(calendarId, events.NextSyncToken).ConfigureAwait(false);
|
||||
|
||||
calendar.SynchronizationDeltaToken = events.NextSyncToken;
|
||||
Console.WriteLine($"Updated sync token for calendar {calendarId}");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error during delta sync for calendar {calendarId}: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes a single event change from delta synchronization
|
||||
/// </summary>
|
||||
/// <param name="googleEvent">The Google Calendar event</param>
|
||||
/// <param name="calendarId">The ID of the calendar containing the event</param>
|
||||
private async Task ProcessDeltaCalendarEventAsync(Event googleEvent, AccountCalendar calendar)
|
||||
{
|
||||
var calendarId = calendar.RemoteCalendarId;
|
||||
|
||||
try
|
||||
{
|
||||
if (googleEvent.Status == "cancelled")
|
||||
{
|
||||
// Handle deleted/canceled events
|
||||
await _gmailChangeProcessor.MarkEventAsDeletedAsync(googleEvent.Id, calendarId);
|
||||
Console.WriteLine($"🗑️ Marked event as deleted: {googleEvent.Summary ?? googleEvent.Id}");
|
||||
return;
|
||||
}
|
||||
|
||||
// For active events (confirmed, tentative), process normally
|
||||
var calendarEvent = GoogleIntegratorExtensions.MapGoogleEventToCalendarEvent(googleEvent, calendar);
|
||||
var result = await _gmailChangeProcessor.UpsertEventAsync(calendarEvent);
|
||||
|
||||
// Sync attendees for delta events too
|
||||
await SyncEventAttendeesAsync(googleEvent, calendarEvent.Id);
|
||||
|
||||
if (result > 0)
|
||||
{
|
||||
var action = await _gmailChangeProcessor.GetEventByRemoteIdAsync(googleEvent.Id) != null ? "Updated" : "Created";
|
||||
Console.WriteLine($"✅ {action} event: {calendarEvent.Title} ({calendarEvent.RemoteEventId})");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"❌ Failed to process delta event {googleEvent.Id}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes a single Google Calendar event and updates the local database
|
||||
/// </summary>
|
||||
/// <param name="googleEvent">The Google Calendar event</param>
|
||||
/// <param name="calendarId">The ID of the calendar containing the event</param>
|
||||
private async Task ProcessGoogleEventAsync(Event googleEvent, AccountCalendar calendar)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (googleEvent.Status == "cancelled")
|
||||
{
|
||||
// Handle deleted events
|
||||
await _gmailChangeProcessor.DeleteEventAsync(googleEvent.Id);
|
||||
Console.WriteLine($"Marked event as deleted: {googleEvent.Summary ?? googleEvent.Id}");
|
||||
return;
|
||||
}
|
||||
|
||||
var calendarEvent = googleEvent.MapGoogleEventToCalendarEvent(calendar);
|
||||
await _gmailChangeProcessor.UpsertEventAsync(calendarEvent);
|
||||
|
||||
// Sync attendees separately
|
||||
await SyncEventAttendeesAsync(googleEvent, calendarEvent.Id);
|
||||
|
||||
Console.WriteLine($"Processed event: {calendarEvent.Title} ({calendarEvent.RemoteEventId})");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Failed to process event {googleEvent.Id}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SynchronizeCalendarsAsync(CancellationToken cancellationToken = default)
|
||||
@@ -511,6 +643,40 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Syncs attendees for an event from Google Calendar data
|
||||
/// </summary>
|
||||
/// <param name="googleEvent">The Google Calendar event</param>
|
||||
/// <param name="eventId">The internal event Guid</param>
|
||||
private async Task SyncEventAttendeesAsync(Event googleEvent, Guid eventId)
|
||||
{
|
||||
var attendees = new List<CalendarEventAttendee>();
|
||||
|
||||
if (googleEvent.Attendees != null && googleEvent.Attendees.Count > 0)
|
||||
{
|
||||
foreach (var googleAttendee in googleEvent.Attendees)
|
||||
{
|
||||
var attendee = new CalendarEventAttendee
|
||||
{
|
||||
EventId = eventId,
|
||||
Email = googleAttendee.Email ?? string.Empty,
|
||||
DisplayName = googleAttendee.DisplayName,
|
||||
ResponseStatus = GoogleIntegratorExtensions.FromGoogleStatus(googleAttendee.ResponseStatus),
|
||||
IsOptional = googleAttendee.Optional ?? false,
|
||||
IsOrganizer = googleAttendee.Organizer ?? false,
|
||||
IsSelf = googleAttendee.Self ?? false,
|
||||
Comment = googleAttendee.Comment,
|
||||
AdditionalGuests = googleAttendee.AdditionalGuests
|
||||
};
|
||||
|
||||
attendees.Add(attendee);
|
||||
}
|
||||
}
|
||||
|
||||
// Sync attendees (replaces existing)
|
||||
await _gmailChangeProcessor.SyncAttendeesForEventAsync(eventId, attendees).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task InitializeArchiveFolderAsync()
|
||||
{
|
||||
var localFolders = await _gmailChangeProcessor.GetLocalFoldersAsync(Account.Id).ConfigureAwait(false);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user