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)
|
||||
|
||||
Reference in New Issue
Block a user