chiark / gitweb /
only register gcal_link if necessary
[gooswapper] / gooswapper.py
index d01d2b38a2e8daaa2ddeaaee0f6150b17b8a3363..75edd3febb2c54bcd77979cfadd513b4268f169e 100644 (file)
@@ -26,6 +26,7 @@ import apiclient.discovery
 import oauth2client
 import oauth2client.file
 import oauth2client.client
+import googleapiclient.errors
 
 #Not sure what the distribution approach here is...
 gcal_client_id = '805127902516-ptbbtgpq9o8pjr6r3k6hsm60j589o85u.apps.googleusercontent.com'
@@ -53,7 +54,18 @@ class ex_gcal_link(exchangelib.ExtendedProperty):
     property_name = "google calendar event id"
     property_type = 'String'
 
-exchangelib.CalendarItem.register('gcal_link',ex_gcal_link)
+try:
+    exchangelib.CalendarItem.get_field_by_fieldname('gcal_link')
+except ValueError:
+    exchangelib.CalendarItem.register('gcal_link',ex_gcal_link)
+
+#useful if you want to replay an event
+def drop_from_ex_cache(itemid):
+    with open(cachepath,"rb") as f:
+        cache = pickle.load(f)
+    cache.pop(itemid)
+    with open(cachepath,"wb") as f:
+        pickle.dump(cache,f)
 
 def get_ex_event_by_itemid(calendar,itemid):
     return calendar.get(item_id=itemid)
@@ -124,34 +136,68 @@ def ex_event_changes(old,new):
                                                               len(deleted)))
     return added, deleted, changed
 
+def rrule_from_ex(event,gcal_tz):
+    if event.type != "RecurringMaster":
+        logger.error("Cannot make recurrence from not-recurring event")
+        return None
+    if event.recurrence is None:
+        logger.error("Empty recurrence structure")
+        return None
+    if isinstance(event.recurrence.pattern,
+                  exchangelib.recurrence.DailyPattern):
+        rr = "RRULE:FREQ=DAILY;INTERVAL=%d" % event.recurrence.pattern.interval
+    else:
+        logger.error("Recurrence %s not supported" % event.recurrence)
+        return None
+    if isinstance(event.recurrence.boundary,
+                  exchangelib.recurrence.EndDatePattern):
+        rr += ";UNTIL={0:%Y}{0:%m}{0:%d}".format(event.recurrence.boundary.end)
+    else:
+        logger.error("Recurrence %s not supported" % event.recurrence)
+        return None
+    if event.modified_occurrences is not None or \
+       event.deleted_occurrences is not None:
+        logger.warning("Modified/Deleted recurrences not supported")
+    return [rr]
+
+def build_gcal_event_from_ex(event,gcal_tz):
+    gevent={}
+    gevent["summary"]=event.subject
+    if event.is_all_day:
+        gevent["end"]={"date": str(event.end.astimezone(gcal_tz).date())}
+        gevent["start"]={"date": str(event.start.astimezone(gcal_tz).date())}
+    else:
+        gevent["end"]={"dateTime": event.end.astimezone(gcal_tz).isoformat(),
+                       "timeZone": str(gcal_tz)}
+        gevent["start"]={"dateTime": event.start.astimezone(gcal_tz).isoformat(),
+                         "timeZone": str(gcal_tz)}
+    if event.text_body is not None and event.text_body.strip() != '':
+        gevent["description"] = event.text_body
+    if event.location is not None:
+        gevent["location"] = event.location
+    gevent["extendedProperties"]={"shared": {"ex_id": event.item_id}}
+    return gevent
+
 def add_ex_to_gcal(ex_acct,
                    gcal_acct,gcal_tz,events,
                    added,
                    gcal_id="primary"):
     for ev_id in added:
         event = get_ex_event_by_itemid(ex_acct.calendar,ev_id)
-        if not event.is_recurring:
-            gevent={}
-            gevent["summary"]=event.subject
-            if event.is_all_day:
-                gevent["end"]={"date": str(event.end.astimezone(gcal_tz).date())}
-                gevent["start"]={"date": str(event.start.astimezone(gcal_tz).date())}
+        gevent = build_gcal_event_from_ex(event,gcal_tz)
+        if event.type=="RecurringMaster":
+            rr = rrule_from_ex(event,gcal_tz)
+            if rr is not None:
+                gevent["recurrence"] = rr
+                print(gevent)
             else:
-                gevent["end"]={"dateTime": event.end.isoformat(),
-                               "timeZone": event.end.tzname()}
-                gevent["start"]={"dateTime": event.start.isoformat(),
-                                 "timeZone": event.start.tzname()}
-            if event.text_body.strip() != '':
-                gevent["description"] = event.text_body
-            if event.location is not None:
-                gevent["location"] = event.location
-            gevent["extendedProperties"]={"shared": {"ex_id": event.item_id}}
-            gevent=gcal_acct.events().insert(calendarId=gcal_id, body=gevent).execute()
-            event.gcal_link = gevent.get("id")
-            event.save()
-            events[event.item_id] = events[event.item_id]._replace(changekey=event.changekey,gcal_link=event.gcal_link)
-        else:
-            logger.warning("only all-day events supported")
+                logger.warning("Unable to set recurrence for %s" % event.item_id)
+                continue #don't make the gcal event
+        gevent = gcal_acct.events().insert(calendarId=gcal_id,
+                                           body=gevent).execute()
+        event.gcal_link = gevent.get("id")
+        event.save(update_fields=["gcal_link"])
+        events[event.item_id] = events[event.item_id]._replace(changekey=event.changekey,gcal_link=event.gcal_link)
 
 def del_ex_to_gcal(ex_acct, gcal_acct, events, deleted, gcal_id="primary"):
     for ev_id in deleted:
@@ -159,6 +205,60 @@ def del_ex_to_gcal(ex_acct, gcal_acct, events, deleted, gcal_id="primary"):
             gcal_acct.events().delete(calendarId=gcal_id,
                                       eventId=events[ev_id].gcal_link,
                                       sendUpdates="none").execute()
+
+def update_ex_to_gcal(ex_acct,
+                      gcal_acct,gcal_tz,
+                      events,changed,
+                      gcal_id="primary"):
+    for ev_id in changed:
+        event = get_ex_event_by_itemid(ex_acct.calendar,ev_id)
+        if not event.is_recurring:
+            gevent = build_gcal_event_from_ex(event,gcal_tz)
+            gevent = gcal_acct.events().update(calendarId=gcal_id,
+                                               eventId=event.gcal_link,
+                                               body=gevent,
+                                               sendUpdates="none").execute()
+        else:
+            logger.warning("recurring events not yet supported")
+
+def match_ex_to_gcal(ex_acct,gcal_acct,gcal_tz,events,gcal_id="primary"):
+    recur = 0
+    matched = 0
+    skipped = 0
+    for ev_id in events:
+        event = get_ex_event_by_itemid(ex_acct.calendar,ev_id)
+        if event.is_recurring:
+            recur += 1
+            continue
+        elif event.gcal_link is not None:
+            skipped += 1
+            continue
+        matches = gcal_acct.events().list(calendarId=gcal_id,
+                                          timeMin=event.start.isoformat(),
+                                          timeMax=event.end.isoformat()).execute()
+        for ge in matches['items']:
+            if ge['summary'].strip()==event.subject.strip():
+                logger.info("Matching '%s' starting at %s" % (event.subject,
+                                                              event.start.isoformat()))
+                event.gcal_link = ge['id']
+                event.save(update_fields=["gcal_link"])
+                events[event.item_id] = events[event.item_id]._replace(changekey=event.changekey,gcal_link=event.gcal_link)
+                gevent = {}
+                gevent["start"] = ge["start"]
+                gevent["end"] = ge["end"]
+                gevent["extendedProperties"]={"shared": {"ex_id": event.item_id}}
+                try:
+                    gcal_acct.events().update(calendarId=gcal_id,
+                                              eventId=event.gcal_link,
+                                              body=gevent,
+                                              sendUpdates="none").execute()
+                #this may fail if we don't own the event
+                except googleapiclient.errors.HttpError as err:
+                    if err.resp.status == 403:
+                        pass
+                matched += 1
+                break
+    logger.info("Matched %d events, skipped %d with existing link, and %d recurring ones" % (matched,skipped,recur))
     
 def get_gcal_cred():
     #each such file can only store a single credential
@@ -203,6 +303,9 @@ def main():
         #delete op needs the "cache" set, as that has the link ids in
         #for events that are now deleted
         del_ex_to_gcal(ex_account,gcal_account,cache,deleted)
+        update_ex_to_gcal(ex_account,gcal_account,gcal_tz,current,changed)
+    else:
+        match_ex_to_gcal(ex_account,gcal_account,gcal_tz,current)
         
     with open(cachepath,"wb") as f:
         pickle.dump(current,f)