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; public class GmailChangeProcessor : DefaultChangeProcessor, IGmailChangeProcessor { public GmailChangeProcessor(IDatabaseService databaseService, IFolderService folderService, IMailService mailService, ICalendarService calendarService, IAccountService accountService, IMimeFileService mimeFileService) : base(databaseService, folderService, mailService, calendarService, accountService, mimeFileService) { } public Task MapLocalDraftAsync(string mailCopyId, string newDraftId, string newThreadId) => MailService.MapLocalDraftAsync(mailCopyId, newDraftId, newThreadId); public Task CreateAssignmentAsync(Guid accountId, string mailCopyId, string remoteFolderId) => MailService.CreateAssignmentAsync(accountId, mailCopyId, remoteFolderId); 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(); 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); } private string GetOrganizerName(Event calendarEvent, MailAccount account) { if (calendarEvent.Organizer == null) return string.Empty; if (calendarEvent.Organizer.Self == true) { return account.SenderName; } else return calendarEvent.Organizer.DisplayName; } private string GetOrganizerEmail(Event calendarEvent, MailAccount account) { if (calendarEvent.Organizer == null) return string.Empty; if (calendarEvent.Organizer.Self == true) { return account.Address; } else return calendarEvent.Organizer.Email; } private CalendarItemStatus GetStatus(string status) { return status switch { "confirmed" => CalendarItemStatus.Confirmed, "tentative" => CalendarItemStatus.Tentative, "cancelled" => CalendarItemStatus.Cancelled, _ => CalendarItemStatus.Confirmed }; } private CalendarItemVisibility GetVisibility(string visibility) { /// Visibility of the event. Optional. Possible values are: - "default" - Uses the default visibility for /// events on the calendar. This is the default value. - "public" - The event is public and event details are /// visible to all readers of the calendar. - "private" - The event is private and only event attendees may /// view event details. - "confidential" - The event is private. This value is provided for compatibility /// reasons. return visibility switch { "default" => CalendarItemVisibility.Default, "public" => CalendarItemVisibility.Public, "private" => CalendarItemVisibility.Private, "confidential" => CalendarItemVisibility.Confidential, _ => CalendarItemVisibility.Default }; } public Task HasAccountAnyDraftAsync(Guid accountId) => MailService.HasAccountAnyDraftAsync(accountId); public Task GetGmailArchiveComparisonResultAsync(Guid archiveFolderId, List onlineArchiveMailIds) => MailService.GetGmailArchiveComparisonResultAsync(archiveFolderId, onlineArchiveMailIds); }