Event creation.

This commit is contained in:
Burak Kaan Köse
2026-03-07 17:13:48 +01:00
parent d1f8163d72
commit ebc35c3de8
21 changed files with 921 additions and 161 deletions
+8 -2
View File
@@ -144,7 +144,10 @@ public abstract partial class BaseSynchronizer<TBaseRequest> : ObservableObject,
if (request is ICalendarActionRequest calendarActionRequest)
{
_pendingCalendarOperationIds.TryAdd(calendarActionRequest.Item.Id, 0);
if (calendarActionRequest.LocalCalendarItemId.HasValue)
{
_pendingCalendarOperationIds.TryAdd(calendarActionRequest.LocalCalendarItemId.Value, 0);
}
}
}
@@ -157,7 +160,10 @@ public abstract partial class BaseSynchronizer<TBaseRequest> : ObservableObject,
if (request is ICalendarActionRequest calendarActionRequest)
{
_pendingCalendarOperationIds.TryRemove(calendarActionRequest.Item.Id, out _);
if (calendarActionRequest.LocalCalendarItemId.HasValue)
{
_pendingCalendarOperationIds.TryRemove(calendarActionRequest.LocalCalendarItemId.Value, out _);
}
}
}
+78 -41
View File
@@ -2276,55 +2276,49 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
public override List<IRequestBundle<IClientServiceRequest>> CreateCalendarEvent(CreateCalendarEventRequest request)
{
var calendarItem = request.Item;
var attendees = request.Attendees;
var calendarItem = request.PreparedItem;
var attendees = request.PreparedEvent.Attendees;
var reminders = request.PreparedEvent.Reminders;
var calendar = request.AssignedCalendar;
// Get the calendar for this event
var calendar = calendarItem.AssignedCalendar;
if (calendar == null)
{
throw new InvalidOperationException("Calendar item must have an assigned calendar");
}
// Convert CalendarItem to Google Event
var googleEvent = new Event
{
Id = calendarItem.Id.ToString("N").ToLowerInvariant(),
Summary = calendarItem.Title,
Description = calendarItem.Description,
Location = calendarItem.Location,
Status = calendarItem.Status == CalendarItemStatus.Accepted ? "confirmed" : "tentative"
Status = calendarItem.Status == CalendarItemStatus.Accepted ? "confirmed" : "tentative",
Transparency = calendarItem.ShowAs == CalendarItemShowAs.Free ? "transparent" : "opaque"
};
// Set start and end time
if (calendarItem.IsAllDayEvent)
{
// All-day events use Date instead of DateTime
googleEvent.Start = new EventDateTime
{
Date = calendarItem.StartDate.ToString("yyyy-MM-dd")
};
googleEvent.End = new EventDateTime
{
Date = calendarItem.EndDate.ToString("yyyy-MM-dd")
};
}
else
{
// Regular events with time
googleEvent.Start = new EventDateTime
{
DateTimeDateTimeOffset = new DateTimeOffset(calendarItem.StartDate, TimeSpan.Zero),
Date = calendarItem.StartDate.ToString("yyyy-MM-dd"),
TimeZone = calendarItem.StartTimeZone
};
googleEvent.End = new EventDateTime
{
DateTimeDateTimeOffset = new DateTimeOffset(calendarItem.EndDate, TimeSpan.Zero),
Date = calendarItem.EndDate.ToString("yyyy-MM-dd"),
TimeZone = calendarItem.EndTimeZone
};
}
else
{
googleEvent.Start = new EventDateTime
{
DateTimeDateTimeOffset = new DateTimeOffset(calendarItem.StartDate, ResolveOffset(calendarItem.StartDate, calendarItem.StartTimeZone)),
TimeZone = calendarItem.StartTimeZone
};
googleEvent.End = new EventDateTime
{
DateTimeDateTimeOffset = new DateTimeOffset(calendarItem.EndDate, ResolveOffset(calendarItem.EndDate, calendarItem.EndTimeZone ?? calendarItem.StartTimeZone)),
TimeZone = calendarItem.EndTimeZone
};
}
// Add attendees if any
if (attendees != null && attendees.Count > 0)
if (attendees.Count > 0)
{
googleEvent.Attendees = attendees.Select(a => new EventAttendee
{
@@ -2334,8 +2328,32 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
}).ToList();
}
// Create the insert request
if (reminders.Count > 0)
{
googleEvent.Reminders = new Event.RemindersData
{
UseDefault = false,
Overrides = reminders.Select(reminder => new EventReminder
{
Method = reminder.ReminderType == CalendarItemReminderType.Email ? "email" : "popup",
Minutes = (int)Math.Max(0, reminder.DurationInSeconds / 60)
}).ToList()
};
}
if (!string.IsNullOrWhiteSpace(calendarItem.Recurrence))
{
googleEvent.Recurrence = calendarItem.Recurrence
.Split(Wino.Core.Domain.Constants.CalendarEventRecurrenceRuleSeperator, StringSplitOptions.RemoveEmptyEntries)
.Select(line => line.Trim())
.Where(line => !string.IsNullOrWhiteSpace(line))
.ToList();
}
var insertRequest = _calendarService.Events.Insert(googleEvent, calendar.RemoteCalendarId);
insertRequest.SendUpdates = attendees.Count > 0
? Google.Apis.Calendar.v3.EventsResource.InsertRequest.SendUpdatesEnum.All
: Google.Apis.Calendar.v3.EventsResource.InsertRequest.SendUpdatesEnum.None;
return [new HttpRequestBundle<IClientServiceRequest>(insertRequest, request)];
}
@@ -2350,7 +2368,8 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
throw new InvalidOperationException("Calendar item must have an assigned calendar");
}
if (string.IsNullOrEmpty(calendarItem.RemoteEventId))
var remoteEventId = calendarItem.RemoteEventId.GetProviderRemoteEventId();
if (string.IsNullOrEmpty(remoteEventId))
{
throw new InvalidOperationException("Cannot accept event without remote event ID");
}
@@ -2375,7 +2394,7 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
ResponseStatus = "accepted"
}
}
}, calendar.RemoteCalendarId, calendarItem.RemoteEventId);
}, calendar.RemoteCalendarId, remoteEventId);
// Send updates to other attendees if there's a message
patchRequest.SendUpdates = !string.IsNullOrEmpty(request.ResponseMessage)
@@ -2395,7 +2414,8 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
throw new InvalidOperationException("Calendar item must have an assigned calendar");
}
if (string.IsNullOrEmpty(calendarItem.RemoteEventId))
var remoteEventId = calendarItem.RemoteEventId.GetProviderRemoteEventId();
if (string.IsNullOrEmpty(remoteEventId))
{
throw new InvalidOperationException("Cannot decline event without remote event ID");
}
@@ -2413,7 +2433,7 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
Comment = request.ResponseMessage
}
}
}, calendar.RemoteCalendarId, calendarItem.RemoteEventId);
}, calendar.RemoteCalendarId, remoteEventId);
patchRequest.SendUpdates = !string.IsNullOrEmpty(request.ResponseMessage)
? Google.Apis.Calendar.v3.EventsResource.PatchRequest.SendUpdatesEnum.All
@@ -2432,7 +2452,8 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
throw new InvalidOperationException("Calendar item must have an assigned calendar");
}
if (string.IsNullOrEmpty(calendarItem.RemoteEventId))
var remoteEventId = calendarItem.RemoteEventId.GetProviderRemoteEventId();
if (string.IsNullOrEmpty(remoteEventId))
{
throw new InvalidOperationException("Cannot tentatively accept event without remote event ID");
}
@@ -2450,7 +2471,7 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
Comment = request.ResponseMessage
}
}
}, calendar.RemoteCalendarId, calendarItem.RemoteEventId);
}, calendar.RemoteCalendarId, remoteEventId);
patchRequest.SendUpdates = !string.IsNullOrEmpty(request.ResponseMessage)
? Google.Apis.Calendar.v3.EventsResource.PatchRequest.SendUpdatesEnum.All
@@ -2471,7 +2492,8 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
throw new InvalidOperationException("Calendar item must have an assigned calendar");
}
if (string.IsNullOrEmpty(calendarItem.RemoteEventId))
var remoteEventId = calendarItem.RemoteEventId.GetProviderRemoteEventId();
if (string.IsNullOrEmpty(remoteEventId))
{
throw new InvalidOperationException("Cannot update event without remote event ID");
}
@@ -2530,7 +2552,7 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
}
// Update the event using Google Calendar API
var updateRequest = _calendarService.Events.Update(googleEvent, calendar.RemoteCalendarId, calendarItem.RemoteEventId);
var updateRequest = _calendarService.Events.Update(googleEvent, calendar.RemoteCalendarId, remoteEventId);
// Send notifications to attendees if the event has attendees
updateRequest.SendUpdates = (attendees != null && attendees.Count > 0)
@@ -2551,13 +2573,13 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
throw new InvalidOperationException("Calendar item must have an assigned calendar");
}
if (string.IsNullOrEmpty(calendarItem.RemoteEventId))
var remoteEventId = calendarItem.RemoteEventId.GetProviderRemoteEventId();
if (string.IsNullOrEmpty(remoteEventId))
{
throw new InvalidOperationException("Cannot delete event without remote event ID");
}
// Delete the event using Google Calendar API
var deleteRequest = _calendarService.Events.Delete(calendar.RemoteCalendarId, calendarItem.RemoteEventId);
var deleteRequest = _calendarService.Events.Delete(calendar.RemoteCalendarId, remoteEventId);
// Send cancellation notifications to attendees
deleteRequest.SendUpdates = Google.Apis.Calendar.v3.EventsResource.DeleteRequest.SendUpdatesEnum.All;
@@ -2576,4 +2598,19 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
_calendarService.Dispose();
_googleHttpClient.Dispose();
}
private static TimeSpan ResolveOffset(DateTime dateTime, string timeZoneId)
{
if (string.IsNullOrWhiteSpace(timeZoneId))
return TimeSpan.Zero;
try
{
return TimeZoneInfo.FindSystemTimeZoneById(timeZoneId).GetUtcOffset(dateTime);
}
catch
{
return TimeSpan.Zero;
}
}
}
+55 -9
View File
@@ -16,6 +16,7 @@ using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Exceptions;
using Wino.Core.Domain.Extensions;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Calendar;
using Wino.Core.Domain.Models.Connectivity;
@@ -95,7 +96,7 @@ public class ImapSynchronizer : WinoSynchronizer<ImapRequest, ImapMessageCreatio
var poolOptions = ImapClientPoolOptions.CreateDefault(Account.ServerInformation, protocolLogStream);
_clientPool = new ImapClientPool(poolOptions);
_localCalendarOperationHandler = new LocalCalendarOperationHandler(Account, _imapChangeProcessor, _calendarService, "local");
_localCalendarOperationHandler = new LocalCalendarOperationHandler(Account, _imapChangeProcessor, _calendarService, _applicationConfiguration.ApplicationDataFolderPath, "local");
_calDavCalendarOperationHandler = new CalDavCalendarOperationHandler(this, Account, _calendarService, _calDavClient);
}
@@ -1531,32 +1532,38 @@ public class ImapSynchronizer : WinoSynchronizer<ImapRequest, ImapMessageCreatio
private readonly MailAccount _account;
private readonly IImapChangeProcessor _changeProcessor;
private readonly ICalendarService _calendarService;
private readonly string _applicationDataFolderPath;
private readonly string _resourceScheme;
public bool RequiresConnectedClient => false;
public LocalCalendarOperationHandler(MailAccount account, IImapChangeProcessor changeProcessor, ICalendarService calendarService, string resourceScheme)
public LocalCalendarOperationHandler(MailAccount account, IImapChangeProcessor changeProcessor, ICalendarService calendarService, string applicationDataFolderPath, string resourceScheme)
{
_account = account;
_changeProcessor = changeProcessor;
_calendarService = calendarService;
_applicationDataFolderPath = applicationDataFolderPath;
_resourceScheme = resourceScheme;
}
public async Task CreateCalendarEventAsync(CreateCalendarEventRequest request)
{
var item = request.Item;
var item = request.PreparedItem;
var attendees = request.PreparedEvent.Attendees;
var reminders = request.PreparedEvent.Reminders;
EnsureCalendarItemDefaults(item, _account, "local");
item.AssignedCalendar ??= await _calendarService.GetAccountCalendarAsync(item.CalendarId).ConfigureAwait(false);
var existing = await _calendarService.GetCalendarItemAsync(item.Id).ConfigureAwait(false);
if (existing == null)
await _calendarService.CreateNewCalendarItemAsync(item, request.Attendees).ConfigureAwait(false);
await _calendarService.CreateNewCalendarItemAsync(item, attendees).ConfigureAwait(false);
else
await _calendarService.UpdateCalendarItemAsync(item, request.Attendees).ConfigureAwait(false);
await _calendarService.UpdateCalendarItemAsync(item, attendees).ConfigureAwait(false);
await PersistIcsAsync(item, request.Attendees).ConfigureAwait(false);
await _calendarService.SaveRemindersAsync(item.Id, reminders).ConfigureAwait(false);
await SaveAttachmentsAsync(request.ComposeResult, item.Id).ConfigureAwait(false);
await PersistIcsAsync(item, attendees).ConfigureAwait(false);
}
public async Task UpdateCalendarEventAsync(UpdateCalendarEventRequest request)
@@ -1616,6 +1623,45 @@ public class ImapSynchronizer : WinoSynchronizer<ImapRequest, ImapMessageCreatio
DateTimeOffset.UtcNow.ToString("O"),
icsContent);
}
private async Task SaveAttachmentsAsync(CalendarEventComposeResult composeResult, Guid calendarItemId)
{
await _calendarService.DeleteAttachmentsAsync(calendarItemId).ConfigureAwait(false);
var attachments = composeResult?.Attachments;
if (attachments == null || attachments.Count == 0)
return;
var attachmentsRoot = Path.Combine(_applicationDataFolderPath, "CalendarAttachments", calendarItemId.ToString("N"));
Directory.CreateDirectory(attachmentsRoot);
var storedAttachments = new List<CalendarAttachment>();
foreach (var attachment in attachments.Where(a => !string.IsNullOrWhiteSpace(a.FilePath) && File.Exists(a.FilePath)))
{
var fileName = string.IsNullOrWhiteSpace(attachment.FileName) ? Path.GetFileName(attachment.FilePath) : attachment.FileName;
var destinationPath = Path.Combine(attachmentsRoot, fileName);
File.Copy(attachment.FilePath, destinationPath, overwrite: true);
storedAttachments.Add(new CalendarAttachment
{
Id = Guid.NewGuid(),
CalendarItemId = calendarItemId,
RemoteAttachmentId = attachment.Id.ToString("N"),
FileName = fileName,
Size = attachment.Size,
ContentType = MimeTypes.GetMimeType(fileName),
IsDownloaded = true,
LocalFilePath = destinationPath,
LastModified = DateTimeOffset.UtcNow
});
}
if (storedAttachments.Count > 0)
{
await _calendarService.InsertOrReplaceAttachmentsAsync(storedAttachments).ConfigureAwait(false);
}
}
}
private sealed class CalDavCalendarOperationHandler : IImapCalendarOperationHandler
@@ -1640,7 +1686,7 @@ public class ImapSynchronizer : WinoSynchronizer<ImapRequest, ImapMessageCreatio
}
public Task CreateCalendarEventAsync(CreateCalendarEventRequest request)
=> UpsertCalendarEventAsync(request.Item, request.Attendees);
=> UpsertCalendarEventAsync(request.PreparedItem, request.PreparedEvent.Attendees);
public Task UpdateCalendarEventAsync(UpdateCalendarEventRequest request)
=> UpsertCalendarEventAsync(request.Item, request.Attendees);
@@ -1654,7 +1700,7 @@ public class ImapSynchronizer : WinoSynchronizer<ImapRequest, ImapMessageCreatio
}
await _calDavClient
.DeleteCalendarEventAsync(connection, calendar, request.Item.RemoteEventId)
.DeleteCalendarEventAsync(connection, calendar, request.Item.RemoteEventId.GetProviderRemoteEventId())
.ConfigureAwait(false);
}
@@ -1689,7 +1735,7 @@ public class ImapSynchronizer : WinoSynchronizer<ImapRequest, ImapMessageCreatio
var icsContent = BuildIcsContent(item, attendees);
await _calDavClient
.UpsertCalendarEventAsync(connection, calendar, item.RemoteEventId, icsContent)
.UpsertCalendarEventAsync(connection, calendar, item.RemoteEventId.GetProviderRemoteEventId(), icsContent)
.ConfigureAwait(false);
}
+136 -48
View File
@@ -27,6 +27,7 @@ using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Exceptions;
using Wino.Core.Domain.Extensions;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Accounts;
using Wino.Core.Domain.Models.Folders;
@@ -34,6 +35,7 @@ using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Synchronization;
using Wino.Core.Extensions;
using Wino.Core.Http;
using Wino.Core.Helpers;
using Wino.Core.Integration.Processors;
using Wino.Core.Misc;
using Wino.Core.Requests.Bundles;
@@ -1634,11 +1636,12 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
try
{
var calendar = calendarItem.AssignedCalendar;
var remoteEventId = calendarItem.RemoteEventId.GetProviderRemoteEventId();
// First, get the attachment metadata to retrieve contentBytes for FileAttachment
var attachmentItem = await _graphClient.Me
.Calendars[calendar.RemoteCalendarId]
.Events[calendarItem.RemoteEventId]
.Events[remoteEventId]
.Attachments[attachment.RemoteAttachmentId]
.GetAsync(cancellationToken: cancellationToken)
.ConfigureAwait(false);
@@ -1879,9 +1882,6 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
private async Task HandleSuccessfulResponseAsync(IRequestBundle<RequestInformation> bundle, HttpResponseMessage response)
{
if (bundle?.UIChangeRequest is not CreateDraftRequest createDraftRequest)
return;
try
{
var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
@@ -1889,24 +1889,95 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
return;
var json = JsonNode.Parse(content);
var createdDraftId = json?["id"]?.GetValue<string>();
if (string.IsNullOrWhiteSpace(createdDraftId))
if (bundle?.UIChangeRequest is CreateDraftRequest createDraftRequest)
{
var createdDraftId = json?["id"]?.GetValue<string>();
if (string.IsNullOrWhiteSpace(createdDraftId))
return;
var createdConversationId = json?["conversationId"]?.GetValue<string>();
var localDraft = createDraftRequest.DraftPreperationRequest.CreatedLocalDraftCopy;
await _outlookChangeProcessor.MapLocalDraftAsync(
Account.Id,
localDraft.UniqueId,
createdDraftId,
createdConversationId,
createdConversationId).ConfigureAwait(false);
return;
}
var createdConversationId = json?["conversationId"]?.GetValue<string>();
var localDraft = createDraftRequest.DraftPreperationRequest.CreatedLocalDraftCopy;
if (bundle?.UIChangeRequest is CreateCalendarEventRequest createCalendarEventRequest)
{
var createdEventId = json?["id"]?.GetValue<string>();
if (string.IsNullOrWhiteSpace(createdEventId))
return;
await _outlookChangeProcessor.MapLocalDraftAsync(
Account.Id,
localDraft.UniqueId,
createdDraftId,
createdConversationId,
createdConversationId).ConfigureAwait(false);
await UploadCalendarEventAttachmentsAsync(createCalendarEventRequest, createdEventId, CancellationToken.None).ConfigureAwait(false);
}
}
catch (Exception ex)
{
// Draft mapping is best-effort here. Delta sync mapping remains as fallback.
_logger.Debug(ex, "Failed to map Outlook draft from create-draft response.");
_logger.Debug(ex, "Failed to process Outlook create response.");
}
}
private async Task UploadCalendarEventAttachmentsAsync(CreateCalendarEventRequest request, string remoteEventId, CancellationToken cancellationToken)
{
var attachments = request.ComposeResult.Attachments ?? [];
if (attachments.Count == 0)
return;
var remoteCalendarId = request.AssignedCalendar.RemoteCalendarId;
foreach (var attachment in attachments.Where(a => !string.IsNullOrWhiteSpace(a.FilePath) && File.Exists(a.FilePath)))
{
cancellationToken.ThrowIfCancellationRequested();
var contentBytes = await File.ReadAllBytesAsync(attachment.FilePath, cancellationToken).ConfigureAwait(false);
var contentType = MimeTypes.GetMimeType(attachment.FileName ?? attachment.FilePath);
var fileAttachment = new FileAttachment
{
Name = attachment.FileName,
ContentType = contentType,
ContentBytes = contentBytes
};
if (contentBytes.Length <= SimpleAttachmentUploadLimitBytes)
{
await _graphClient.Me.Calendars[remoteCalendarId].Events[remoteEventId].Attachments.PostAsync(fileAttachment, cancellationToken: cancellationToken).ConfigureAwait(false);
continue;
}
if (contentBytes.Length > MaximumUploadSessionAttachmentSizeBytes)
{
var attachmentSizeMb = contentBytes.LongLength / (1024d * 1024d);
var maximumSizeMb = MaximumUploadSessionAttachmentSizeBytes / (1024d * 1024d);
throw new InvalidOperationException(
$"Attachment '{attachment.FileName}' is {attachmentSizeMb:F1} MB, which exceeds Outlook's upload limit of {maximumSizeMb:F0} MB per attachment.");
}
var sessionBody = new Microsoft.Graph.Me.Calendars.Item.Events.Item.Attachments.CreateUploadSession.CreateUploadSessionPostRequestBody
{
AttachmentItem = new AttachmentItem
{
AttachmentType = AttachmentType.File,
ContentType = contentType,
Name = attachment.FileName,
Size = contentBytes.LongLength
}
};
var uploadSession = await _graphClient.Me.Calendars[remoteCalendarId].Events[remoteEventId].Attachments.CreateUploadSession.PostAsync(sessionBody, cancellationToken: cancellationToken).ConfigureAwait(false);
if (uploadSession?.UploadUrl == null)
{
throw new InvalidOperationException($"Failed to create upload session for attachment '{attachment.FileName}'.");
}
await UploadAttachmentInChunksAsync(uploadSession.UploadUrl, contentBytes, cancellationToken).ConfigureAwait(false);
}
}
@@ -2370,52 +2441,51 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
public override List<IRequestBundle<RequestInformation>> CreateCalendarEvent(CreateCalendarEventRequest request)
{
var calendarItem = request.Item;
var attendees = request.Attendees;
var calendarItem = request.PreparedItem;
var attendees = request.PreparedEvent.Attendees;
var reminders = request.PreparedEvent.Reminders;
var calendar = request.AssignedCalendar;
// Get the calendar for this event
var calendar = calendarItem.AssignedCalendar;
if (calendar == null)
{
throw new InvalidOperationException("Calendar item must have an assigned calendar");
}
// Convert CalendarItem to Outlook Event
var outlookEvent = new Microsoft.Graph.Models.Event
{
Subject = calendarItem.Title,
Body = new Microsoft.Graph.Models.ItemBody
{
ContentType = Microsoft.Graph.Models.BodyType.Text,
ContentType = Microsoft.Graph.Models.BodyType.Html,
Content = calendarItem.Description
},
Location = new Microsoft.Graph.Models.Location
{
DisplayName = calendarItem.Location
}
},
ShowAs = calendarItem.ShowAs switch
{
CalendarItemShowAs.Free => Microsoft.Graph.Models.FreeBusyStatus.Free,
CalendarItemShowAs.Tentative => Microsoft.Graph.Models.FreeBusyStatus.Tentative,
CalendarItemShowAs.Busy => Microsoft.Graph.Models.FreeBusyStatus.Busy,
CalendarItemShowAs.OutOfOffice => Microsoft.Graph.Models.FreeBusyStatus.Oof,
CalendarItemShowAs.WorkingElsewhere => Microsoft.Graph.Models.FreeBusyStatus.WorkingElsewhere,
_ => Microsoft.Graph.Models.FreeBusyStatus.Busy
},
TransactionId = calendarItem.Id.ToString("N")
};
// Set start and end time using DateTimeTimeZone
if (calendarItem.IsAllDayEvent)
{
// All-day events
outlookEvent.IsAllDay = true;
outlookEvent.Start = new Microsoft.Graph.Models.DateTimeTimeZone
{
DateTime = calendarItem.StartDate.ToString("yyyy-MM-dd"),
TimeZone = "UTC"
TimeZone = calendarItem.StartTimeZone ?? TimeZoneInfo.Local.Id
};
outlookEvent.End = new Microsoft.Graph.Models.DateTimeTimeZone
{
DateTime = calendarItem.EndDate.ToString("yyyy-MM-dd"),
TimeZone = "UTC"
TimeZone = calendarItem.EndTimeZone ?? calendarItem.StartTimeZone ?? TimeZoneInfo.Local.Id
};
}
else
{
// Regular events with time
// StartDate and EndDate are stored in the event's timezone
// We preserve the timezone information during creation
outlookEvent.IsAllDay = false;
outlookEvent.Start = new Microsoft.Graph.Models.DateTimeTimeZone
{
@@ -2429,8 +2499,7 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
};
}
// Add attendees if any
if (attendees != null && attendees.Count > 0)
if (attendees.Count > 0)
{
outlookEvent.Attendees = attendees.Select(a => new Microsoft.Graph.Models.Attendee
{
@@ -2443,7 +2512,23 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
}).ToList();
}
// Create the event using Graph API
if (reminders.Count > 0)
{
var reminder = reminders
.OrderBy(reminder => reminder.DurationInSeconds)
.FirstOrDefault(reminder => reminder.ReminderType == CalendarItemReminderType.Popup)
?? reminders.OrderBy(reminder => reminder.DurationInSeconds).First();
outlookEvent.IsReminderOn = true;
outlookEvent.ReminderMinutesBeforeStart = (int)Math.Max(0, reminder.DurationInSeconds / 60);
}
var recurrence = CalendarRecurrenceMapper.CreateOutlookRecurrence(calendarItem);
if (recurrence != null)
{
outlookEvent.Recurrence = recurrence;
}
var createRequest = _graphClient.Me.Calendars[calendar.RemoteCalendarId].Events.ToPostRequestInformation(outlookEvent);
return [new HttpRequestBundle<RequestInformation>(createRequest, request)];
@@ -2459,12 +2544,13 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
throw new InvalidOperationException("Calendar item must have an assigned calendar");
}
if (string.IsNullOrEmpty(calendarItem.RemoteEventId))
var remoteEventId = calendarItem.RemoteEventId.GetProviderRemoteEventId();
if (string.IsNullOrEmpty(remoteEventId))
{
throw new InvalidOperationException("Cannot accept event without remote event ID");
}
var acceptRequestInfo = _graphClient.Me.Calendars[calendar.RemoteCalendarId].Events[calendarItem.RemoteEventId].Accept.ToPostRequestInformation(new Microsoft.Graph.Me.Calendars.Item.Events.Item.Accept.AcceptPostRequestBody
var acceptRequestInfo = _graphClient.Me.Calendars[calendar.RemoteCalendarId].Events[remoteEventId].Accept.ToPostRequestInformation(new Microsoft.Graph.Me.Calendars.Item.Events.Item.Accept.AcceptPostRequestBody
{
Comment = request.ResponseMessage,
SendResponse = !string.IsNullOrEmpty(request.ResponseMessage)
@@ -2485,12 +2571,13 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
throw new InvalidOperationException("Calendar item must have an assigned calendar");
}
if (string.IsNullOrEmpty(calendarItem.RemoteEventId))
var remoteEventId = calendarItem.RemoteEventId.GetProviderRemoteEventId();
if (string.IsNullOrEmpty(remoteEventId))
{
throw new InvalidOperationException("Cannot decline event without remote event ID");
}
var declineRequestInfo = _graphClient.Me.Calendars[calendar.RemoteCalendarId].Events[calendarItem.RemoteEventId].Decline.ToPostRequestInformation(new Microsoft.Graph.Me.Calendars.Item.Events.Item.Decline.DeclinePostRequestBody
var declineRequestInfo = _graphClient.Me.Calendars[calendar.RemoteCalendarId].Events[remoteEventId].Decline.ToPostRequestInformation(new Microsoft.Graph.Me.Calendars.Item.Events.Item.Decline.DeclinePostRequestBody
{
Comment = responseMessage,
SendResponse = !string.IsNullOrEmpty(responseMessage)
@@ -2509,12 +2596,13 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
throw new InvalidOperationException("Calendar item must have an assigned calendar");
}
if (string.IsNullOrEmpty(calendarItem.RemoteEventId))
var remoteEventId = calendarItem.RemoteEventId.GetProviderRemoteEventId();
if (string.IsNullOrEmpty(remoteEventId))
{
throw new InvalidOperationException("Cannot tentatively accept event without remote event ID");
}
var tentativelyAcceptRequestInfo = _graphClient.Me.Calendars[calendar.RemoteCalendarId].Events[calendarItem.RemoteEventId].TentativelyAccept.ToPostRequestInformation(new Microsoft.Graph.Me.Calendars.Item.Events.Item.TentativelyAccept.TentativelyAcceptPostRequestBody
var tentativelyAcceptRequestInfo = _graphClient.Me.Calendars[calendar.RemoteCalendarId].Events[remoteEventId].TentativelyAccept.ToPostRequestInformation(new Microsoft.Graph.Me.Calendars.Item.Events.Item.TentativelyAccept.TentativelyAcceptPostRequestBody
{
Comment = request.ResponseMessage,
SendResponse = !string.IsNullOrEmpty(request.ResponseMessage)
@@ -2608,7 +2696,7 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
}
// Update the event using Graph API
var updateRequest = _graphClient.Me.Events[calendarItem.RemoteEventId].ToPatchRequestInformation(outlookEvent);
var updateRequest = _graphClient.Me.Events[calendarItem.RemoteEventId.GetProviderRemoteEventId()].ToPatchRequestInformation(outlookEvent);
return [new HttpRequestBundle<RequestInformation>(updateRequest, request)];
}
@@ -2624,13 +2712,13 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
throw new InvalidOperationException("Calendar item must have an assigned calendar");
}
if (string.IsNullOrEmpty(calendarItem.RemoteEventId))
var remoteEventId = calendarItem.RemoteEventId.GetProviderRemoteEventId();
if (string.IsNullOrEmpty(remoteEventId))
{
throw new InvalidOperationException("Cannot delete event without remote event ID");
}
// Delete the event using Graph API
var deleteRequest = _graphClient.Me.Calendars[calendar.RemoteCalendarId].Events[calendarItem.RemoteEventId].ToDeleteRequestInformation();
var deleteRequest = _graphClient.Me.Calendars[calendar.RemoteCalendarId].Events[remoteEventId].ToDeleteRequestInformation();
return [new HttpRequestBundle<RequestInformation>(deleteRequest, request)];
}