Skip to content

Commit

Permalink
O->G: Delete and update exceptions for new recurring series.
Browse files Browse the repository at this point in the history
  • Loading branch information
phw198 committed Jan 4, 2025
1 parent 7d6baea commit 3853e6c
Show file tree
Hide file tree
Showing 8 changed files with 104 additions and 108 deletions.
11 changes: 11 additions & 0 deletions src/OutlookGoogleCalendarSync/Extensions/MsGraph.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,16 @@ public static String BodyInnerHtml(this Microsoft.Graph.ItemBody body) {
if (bodyInnerHtml == "<div></div>") return "";
else return bodyInnerHtml;
}

/// <summary>Add the Authorization header to an HTTP Request Message</summary>
public static System.Net.Http.HttpRequestMessage AddAuthorisation(this System.Net.Http.HttpRequestMessage a) {
//This is required due to
// 1. cancelledOccurrences Graph Event property being on the v1.0 API, but undocumented
// 2. the Graph SDK only supporting that property on the beta release channel
// 3. Native GetHttpRequestMessage() to build custom API call doesn't utilise existing Authorization header; https://github.com/microsoftgraph/msgraph-sdk-dotnet/issues/263

a.Headers.Authorization = new("Bearer", Ogcs.Outlook.Graph.Calendar.Instance.Authenticator.AccessToken);
return a;
}
}
}
7 changes: 4 additions & 3 deletions src/OutlookGoogleCalendarSync/Google.Graph/GoogleCalendar.cs
Original file line number Diff line number Diff line change
Expand Up @@ -539,11 +539,12 @@ public static void CreateCalendarEntries(List<Microsoft.Graph.Event> appointment
else
throw new UserCancelledSyncException("User chose not to continue sync.");
}
/*if (ai.IsRecurring && Recurrence.HasExceptions(ai) && createdEvent != null) {
List<Microsoft.Graph.Event> aiExcps;
if (createdEvent != null && ai.Recurrence != null && (aiExcps = Outlook.Graph.Recurrence.GetExceptions(ai)).Count > 0) {
Forms.Main.Instance.Console.Update("This is a recurring item with some exceptions:-", verbose: true);
Recurrence.CreateGoogleExceptions(ai, createdEvent.Id);
Recurrence.CreateGoogleExceptions(aiExcps, createdEvent.Id);
Forms.Main.Instance.Console.Update("Recurring exceptions completed.", verbose: true);
}*/
}
}
}

Expand Down
139 changes: 51 additions & 88 deletions src/OutlookGoogleCalendarSync/Google.Graph/GoogleRecurrence.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public class Recurrence {
private static readonly ILog log = LogManager.GetLogger(typeof(Recurrence));

public static List<String> BuildGooglePattern(Microsoft.Graph.Event ai, GcalData.Event ev) {
if (ai.Recurrence == null) return null;
if (ai.Recurrence == null || ai.Type != EventType.SeriesMaster) return null;

log.Debug("Creating Google iCalendar definition for recurring event.");
List<String> gPattern = new List<String>();
Expand Down Expand Up @@ -337,108 +337,71 @@ public static Event GetGoogleMasterEvent(AppointmentItem ai) {
log.Warn("Failed to find master Google event for: " + Outlook.Calendar.GetEventSummary(ai));
return null;
}
*/

public static void CreateGoogleExceptions(AppointmentItem ai, String recurringEventId) {
if (!ai.IsRecurring) return;
public static void CreateGoogleExceptions(List<Event> aiExcps, String recurringEventId) {
if (aiExcps.Count == 0) return;

log.Debug("Creating Google recurrence exceptions.");
List<Event> gRecurrences = Ogcs.Google.Calendar.Instance.GetCalendarEntriesInRecurrence(recurringEventId);
List<GcalData.Event> gRecurrences = Ogcs.Google.Calendar.Instance.GetCalendarEntriesInRecurrence(recurringEventId);
if (gRecurrences == null) return;

RecurrencePattern rp = null;
Exceptions excps = null;
try {
rp = ai.GetRecurrencePattern();
excps = rp.Exceptions;
for (int e = 1; e <= excps.Count; e++) {
Microsoft.Office.Interop.Outlook.Exception oExcp = null;
try {
oExcp = excps[e];
for (int g = 0; g < gRecurrences.Count; g++) {
Event ev = gRecurrences[g];
System.DateTime gDate = ev.OriginalStartTime.SafeDateTime();
Outlook.Recurrence.DeletionState isDeleted = Outlook.Recurrence.ExceptionIsDeleted(oExcp);
if (isDeleted == Outlook.Recurrence.DeletionState.Inaccessible) {
log.Warn("Abandoning creation of Google recurrence exception as Outlook exception is inaccessible.");
return;
}
if (isDeleted == Outlook.Recurrence.DeletionState.Deleted && !ai.AllDayEvent) { //Deleted items get truncated?!
gDate = gDate.Date;
List<System.DateTime> cancelledDates = new();
if (Ogcs.Outlook.Graph.Calendar.Instance.CancelledOccurrences.TryGetValue(aiExcps.First().SeriesMasterId, out cancelledDates)) {
log.Debug($"Cancelling {cancelledDates.Count()} occurrences.");
foreach (System.DateTime cancelledDate in cancelledDates) {
GcalData.Event ev = gRecurrences.Where(ev => ev.OriginalStartTime.SafeDateTime().Date == cancelledDate.Date).First();
if (ev == null) {
log.Warn($"Could not find a Google occurrence for Outlook's cancellation on {cancelledDate}");
} else {
gRecurrences.Remove(ev);
if (ev.Status == "cancelled") {
log.Warn($"Outlook occurrence already deleted on {cancelledDate}");
} else {
Forms.Main.Instance.Console.Update(Ogcs.Google.Calendar.GetEventSummary("<br/>Occurrence deleted.", ev, out String anonSummary), anonSummary, Console.Markup.calendar, verbose: true);
ev.Status = "cancelled";
Ogcs.Google.Calendar.Instance.UpdateCalendarEntry_save(ref ev);
}
if (oExcp.OriginalDate == gDate) {
if (isDeleted == Outlook.Recurrence.DeletionState.Deleted) {
log.Fine("Checking if there are other exceptions that were originally on " + oExcp.OriginalDate.ToString("dd-MMM-yyyy") + " and moved.");
Boolean skipDelete = false;
for (int a = 1; a <= excps.Count; a++) {
Microsoft.Office.Interop.Outlook.Exception oExcp2 = null;
try {
oExcp2 = excps[a];
if (!oExcp2.Deleted) {
Microsoft.Office.Interop.Outlook.AppointmentItem ai2 = null;
try {
ai2 = oExcp2.AppointmentItem;
if (oExcp.OriginalDate.Date == oExcp2.OriginalDate.Date && oExcp.OriginalDate.Date != ai2.Start.Date) {
//It's an additional exception which has the same original start date, but was moved
log.Warn(Ogcs.Google.Calendar.GetEventSummary(ev));
log.Warn("This item is not really deleted, but moved to another date in Outlook on " + ai2.Start.Date.ToString("dd-MMM-yyyy"));
skipDelete = true;
log.Fine("Now checking if there is a Google item on that date - we don't want a duplicate.");
Event duplicate = gRecurrences.FirstOrDefault(g => ai2.Start.Date == g.OriginalStartTime.SafeDateTime().Date);
if (duplicate != null) {
log.Warn("Determined a 'duplicate' exists on that date - this will be deleted.");
duplicate.Status = "cancelled";
Ogcs.Google.Calendar.Instance.UpdateCalendarEntry_save(ref duplicate);
}
break;
}
} catch (System.Exception ex) {
Ogcs.Exception.Analyse(ex);
} finally {
ai2 = (Microsoft.Office.Interop.Outlook.AppointmentItem)Outlook.Calendar.ReleaseObject(ai2);
}
}
} catch (System.Exception ex) {
ex.Analyse("Could not check if there are other exceptions with the same original start date.");
}
}
if (!skipDelete) {
log.Fine("None found.");
Forms.Main.Instance.Console.Update(Ogcs.Google.Calendar.GetEventSummary("<br/>Occurrence deleted.", ev, out String anonSummary), anonSummary, Console.Markup.calendar, verbose: true);
ev.Status = "cancelled";
Ogcs.Google.Calendar.Instance.UpdateCalendarEntry_save(ref ev);
}
} else {
int exceptionItemsModified = 0;
Event modifiedEv = Ogcs.Google.Calendar.Instance.UpdateCalendarEntry(oExcp.AppointmentItem, ev, ref exceptionItemsModified, forceCompare: true);
if (exceptionItemsModified > 0) {
Ogcs.Google.Calendar.Instance.UpdateCalendarEntry_save(ref modifiedEv);
if (oExcp.OriginalDate.Date != oExcp.AppointmentItem.Start.Date) {
log.Fine("Double checking there is no other Google item on " + oExcp.AppointmentItem.Start.Date.ToString("dd-MMM-yyyy") + " that " + oExcp.OriginalDate.Date.ToString("dd-MMM-yyyy") + " was moved to - we don't want a duplicate.");
Event duplicate = gRecurrences.FirstOrDefault(g => oExcp.AppointmentItem.Start.Date == g.OriginalStartTime.SafeDateTime().Date);
if (duplicate != null && duplicate.Status != "cancelled") {
log.Warn("Determined a 'duplicate' exists on that date - this will be deleted.");
duplicate.Status = "cancelled";
Ogcs.Google.Calendar.Instance.UpdateCalendarEntry_save(ref duplicate);
}
}
}
}
}
} catch (System.Exception ex) {
ex.Analyse("Could not process cancelled occurrences.");
}
try {
foreach (Event oExcp in aiExcps) {
System.DateTime? oOriginalStart = null;
try {
oOriginalStart = (oExcp.OriginalStart?.DateTime ?? oExcp.Start.SafeDateTime()).ToLocalTime();
GcalData.Event ev = gRecurrences.Where(ev => ev.OriginalStartTime.SafeDateTime() == oOriginalStart).FirstOrDefault();
if (ev == null) {
log.Warn($"Could not find an occurrence originally starting on {oOriginalStart}");
} else {
int exceptionItemsModified = 0;
GcalData.Event modifiedEv = Calendar.UpdateCalendarEntry(oExcp, ev, ref exceptionItemsModified, forceCompare: true);
if (exceptionItemsModified > 0) {
Ogcs.Google.Calendar.Instance.UpdateCalendarEntry_save(ref modifiedEv);
if (oExcp.OriginalStart?.Date != oExcp.Start.SafeDateTime().Date) {
log.Fine("Double checking there is no other Google item on " + oExcp.Start.SafeDateTime().Date.ToString("dd-MMM-yyyy") + " that " + oExcp.OriginalStart?.Date.ToString("dd-MMM-yyyy") + " was moved to - we don't want a duplicate.");
GcalData.Event duplicate = gRecurrences.FirstOrDefault(g => oExcp.Start.SafeDateTime().Date == g.OriginalStartTime.SafeDateTime().Date);
if (duplicate != null && duplicate.Status != "cancelled") {
log.Warn("Determined a 'duplicate' exists on that date - this will be deleted.");
duplicate.Status = "cancelled";
Ogcs.Google.Calendar.Instance.UpdateCalendarEntry_save(ref duplicate);
}
}
break;
}
}
} finally {
oExcp = (Microsoft.Office.Interop.Outlook.Exception)Outlook.Calendar.ReleaseObject(oExcp);
} catch (System.Exception ex) {
ex.Analyse($"Failed to process modified occurrence on {oOriginalStart}");
}
}
} finally {
for (int e = 1; e <= excps.Count; e++) {
Microsoft.Office.Interop.Outlook.Exception garbage = (Microsoft.Office.Interop.Outlook.Exception)Outlook.Calendar.ReleaseObject(excps[e]);
}
excps = (Exceptions)Outlook.Calendar.ReleaseObject(excps);
rp = (RecurrencePattern)Outlook.Calendar.ReleaseObject(rp);
} catch (System.Exception ex) {
ex.Analyse("Could not process modified occurrences.");
}
}
/*
public static void UpdateGoogleExceptions(AppointmentItem ai, Event ev, Boolean dirtyCache) {
if (ai.IsRecurring) {
RecurrencePattern rp = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ public Authenticator() {
public Boolean AgedAccessToken {
get { return authResult?.ExpiresOn.UtcDateTime < DateTime.UtcNow.AddMinutes(1); }
}
public String AccessToken {
get { return authResult.AccessToken; }
}

public void GetAuthenticated(Boolean nonInteractiveAuth = false) {
if (this.Authenticated && authResult != null && !AgedAccessToken) return;
Expand Down
25 changes: 23 additions & 2 deletions src/OutlookGoogleCalendarSync/Outlook.Graph/O365Calendar.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ public GraphServiceClient GraphClient {

public Graph.EphemeralProperties EphemeralProperties = new Graph.EphemeralProperties();

/// <summary>
/// Graph API v1.0 doesn't properly surface cancelled series occurrences as of Jan-2025
/// Therefore home-brewing our own dictionary workaround.
/// </summary>
public Dictionary<String, List<System.DateTime>> CancelledOccurrences { get; set; }

private Dictionary<String, OutlookCalendarListEntry> calendarFolders = new Dictionary<string, OutlookCalendarListEntry>();
public Dictionary<String, OutlookCalendarListEntry> CalendarFolders {
get { return calendarFolders; }
Expand Down Expand Up @@ -101,12 +107,26 @@ public Event GetCalendarEntry(String eventId) {
log.Debug("Retrieving specific Graph Event with ID " + eventId);
IEventRequest er = GraphClient.Me.Calendars[profile.UseOutlookCalendar.Id].Events[eventId].Request();
er.Expand("extensions($filter=Id eq '" + CustomProperty.ExtensionName() + "')");
ai = er.GetAsync().Result;
er.Select("*"); //This returns undocumented "hidden" beta property cancelledOccurrences
System.Net.Http.HttpRequestMessage httpMessage = er.GetHttpRequestMessage().AddAuthorisation();
System.Net.Http.HttpResponseMessage response = graphClient.HttpProvider.SendAsync(httpMessage).Result;
String jsonContent = response.Content.ReadAsStringAsync().Result;
ai = Newtonsoft.Json.JsonConvert.DeserializeObject<Event>(jsonContent, new Newtonsoft.Json.JsonSerializerSettings { DateParseHandling = Newtonsoft.Json.DateParseHandling.None } );
Newtonsoft.Json.Linq.JToken tk = Newtonsoft.Json.Linq.JObject.Parse(jsonContent).SelectToken("cancelledOccurrences");
foreach (String cancelledOccurrence in tk) {
System.DateTime cancelledDate = System.DateTime.ParseExact(cancelledOccurrence.Replace($"OID.{eventId}.", ""), "yyyy-MM-dd", System.Globalization.CultureInfo.InvariantCulture);
if (CancelledOccurrences.ContainsKey(eventId))
CancelledOccurrences[eventId].Add(cancelledDate);
else
CancelledOccurrences.Add(eventId, new List<System.DateTime>() { cancelledDate });
}

if (ai != null)
return ai;
else
throw new System.Exception("Returned null");
} catch (System.Exception) {
} catch (System.Exception ex) {
ex.Analyse();
Forms.Main.Instance.Console.Update("Failed to retrieve Graph event.", Console.Markup.error);
return null;
}
Expand Down Expand Up @@ -169,6 +189,7 @@ public Event GetCalendarEntry(String eventId) {

req.Top(250);
req.Expand("extensions($filter=Id eq '" + CustomProperty.ExtensionName() + "')");
req.Select("*"); //Otherwise OriginalStart is always null
log.Fine(req.GetHttpRequestMessage().RequestUri.ToString());

Int16 pageNum = 1;
Expand Down
18 changes: 5 additions & 13 deletions src/OutlookGoogleCalendarSync/Outlook.Graph/O365Recurrence.cs
Original file line number Diff line number Diff line change
Expand Up @@ -242,24 +242,16 @@ public static PatternedRecurrence CompareOutlookPattern(GcalData.Event ev, Patte
public static List<Event> OutlookExceptions {
get { return outlookExceptions; }
}
/*public enum DeletionState {
/*
public enum DeletionState {
Inaccessible,
Deleted,
NotDeleted
}
public static Boolean HasExceptions(AppointmentItem ai) {
RecurrencePattern rp = null;
Exceptions excps = null;
try {
rp = ai.GetRecurrencePattern();
excps = rp.Exceptions;
return excps.Count != 0;
} finally {
excps = (Exceptions)Outlook.Calendar.ReleaseObject(excps);
rp = (RecurrencePattern)Outlook.Calendar.ReleaseObject(rp);
}
}
*/
public static List<Event> GetExceptions(Event ai) {
return outlookExceptions.Where(aiExcp => aiExcp.SeriesMasterId == ai.Id).ToList();
}
public static void SeparateOutlookExceptions(List<Event> allAppointments) {
outlookExceptions = new List<Event>();
if (allAppointments.Count == 0) return;
Expand Down
Loading

0 comments on commit 3853e6c

Please sign in to comment.