chiark / gitweb /
Initial commit
authorMatthew Vernon <mv3@sanger.ac.uk>
Fri, 5 Oct 2018 08:31:07 +0000 (09:31 +0100)
committerMatthew Vernon <mv3@sanger.ac.uk>
Fri, 5 Oct 2018 08:31:07 +0000 (09:31 +0100)
This now can cope with new all-day events from exchange, and make the
corresponding event in gcal.

gooswapper.py [new file with mode: 0644]

diff --git a/gooswapper.py b/gooswapper.py
new file mode 100644 (file)
index 0000000..7848916
--- /dev/null
@@ -0,0 +1,194 @@
+#!/usr/bin/env python3
+
+import sys
+import getpass
+import os
+import pickle
+import logging
+logger = logging.getLogger('gooswapper')
+logger.setLevel(logging.INFO)
+consolelog = logging.StreamHandler()
+consolelog.setLevel(logging.INFO)
+logformatter = logging.Formatter('%(name)s - %(levelname)s - %(message)s')
+consolelog.setFormatter(logformatter)
+logger.addHandler(consolelog)
+#We can't use this, because that way all the libraries' logs spam us
+#logging.basicConfig(level=logging.INFO)
+
+#Exchange-related library
+sys.path.append("/upstreams/exchangelib")
+import exchangelib
+
+#Google calendar-api libraries
+import httplib2
+import apiclient.discovery
+import oauth2client
+import oauth2client.file
+import oauth2client.client
+
+#Not sure what the distribution approach here is...
+gcal_client_id = '805127902516-ptbbtgpq9o8pjr6r3k6hsm60j589o85u.apps.googleusercontent.com'
+gcal_client_secret = '8hpdxV3MauorryTDoZ1YK8JO'
+
+#scope URL for r/w calendar access
+scope = 'https://www.googleapis.com/auth/calendar'
+#flow object, for doing OAuth2.0 stuff
+flow = oauth2client.client.OAuth2WebServerFlow(gcal_client_id,
+                                               gcal_client_secret,
+                                               scope)
+
+
+gcal_authpath=".gooswap_gcal_creds.dat"
+
+cachepath=".gooswapcache"
+
+exchange_credential = None
+
+class ex_gcal_link(exchangelib.ExtendedProperty):
+    distinguished_property_set_id = 'PublicStrings'
+    #property_set_id = '12345678-1234-1234-1234-123456781234'
+    #    property_set_id = "gooswapper"
+    property_name = "google calendar event id"
+    property_type = 'String'
+
+exchangelib.CalendarItem.register('gcal_link',ex_gcal_link)
+
+#see docs for exchangelib.UID for why this is needed
+class GlobalObjectId(exchangelib.ExtendedProperty):
+     distinguished_property_set_id = 'Meeting'
+     property_id = 3
+     property_type = 'Binary'
+
+exchangelib.CalendarItem.register('global_object_id', GlobalObjectId)
+
+def get_ex_event_by_uid(calendar,uid):
+    return calendar.get(global_object_id=GlobalObjectId(exchangelib.UID(uid)))
+
+def get_ex_event_by_id_and_changekey(acct,itemid,changekey):
+    l=list(acct.fetch([(itemid,changekey)]))
+    return list(acct.fetch([(itemid,changekey)]))[0]
+
+def get_ex_cred(username="SANGER\mv3",password=None):
+    if password is None:
+        password = getpass.getpass(prompt="Password for user %s: " % username)
+    return exchangelib.ServiceAccount(username,password)
+#    return exchangelib.Credentials(username,password)
+
+def ex_login(emailaddr,autodiscover=True):
+    global exchange_credential
+    if exchange_credential is None:
+        exchange_credential = get_ex_cred()
+    return exchangelib.Account(emailaddr,
+                               credentials = exchange_credential,
+                               autodiscover = autodiscover)
+
+def get_ex_events(calendar):
+    ans={}
+    itemids={}
+    for event in calendar.all().only('uid','changekey','item_id','gcal_link'):
+        if event.gcal_link is not None:
+#            event.delete()
+            continue
+        if event.uid in ans:
+            logger.warning("Event uid %s was duplicated!" % event.uid)
+        ans[event.uid] = event.changekey
+        itemids[event.uid] = event.item_id
+    logger.info("%d events found" % len(ans))
+    return (ans,itemids)
+
+def ex_event_changes(old,new):
+    olds = set(old.keys())
+    news = set(new.keys())
+    added = list(news-olds)
+    deleted = list(olds-news)
+    changed = []
+    #intersection - i.e. common to both sets
+    for event in olds & news:
+        if old[event] != new[event]:
+            changed.append(event)
+    logger.info("%d events updated, %d added, %d deleted" % (len(changed),
+                                                              len(added),
+                                                              len(deleted)))
+    return added, deleted, changed
+
+#XXX doesn't work - cf https://github.com/ecederstrand/exchangelib/issues/492
+def add_ex_to_gcal_needs_ev_id(ex_cal,events):
+    for ev_id in events:
+        print(ev_id)
+        event = get_ex_event_by_uid(ex_cal,ev_id)
+        event.gcal_link = "Testing"
+        event.save()
+
+def add_ex_to_gcal(ex_acct,
+                   gcal_acct,gcal_tz,events,
+                   itemids,added,
+                   gcal_id="primary"):
+    for ev_id in added:
+        event = get_ex_event_by_id_and_changekey(ex_acct,
+                                                 itemids[ev_id],events[ev_id])
+        if event.is_all_day:
+            gevent={}
+            gevent["summary"]=event.subject
+            gevent["end"]={"date": str(event.end.astimezone(gcal_tz).date())}
+            gevent["start"]={"date": str(event.start.astimezone(gcal_tz).date())}
+            if event.text_body.strip() != '':
+                gevent["description"] = event.text_body
+            if event.location is not None:
+                gevent["location"] = event.location
+            gevent["extended_properties"]={"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.uid] = event.changekey
+        else:
+            logger.warning("only all-day events supported")
+            
+def get_gcal_cred():
+    #each such file can only store a single credential
+    storage = oauth2client.file.Storage(gcal_authpath)
+    gcal_credential = storage.get()
+    #if no credential found, or they're invalid (e.g. expired),
+    #then get a new one; pass --noauth_local_webserver on the command line
+    #if you don't want it to spawn a browser
+    if gcal_credential is None or gcal_credential.invalid:
+        gcal_credential = oauth2client.tools.run_flow(flow,
+                                                      storage,
+                                                      oauth2client.tools.argparser.parse_args())
+    return gcal_credential
+
+def gcal_login():
+    gcal_credential = get_gcal_cred()
+    # Object to handle http requests; could add proxy details
+    http = httplib2.Http()
+    http = gcal_credential.authorize(http)
+    return apiclient.discovery.build('calendar', 'v3', http=http)
+
+def get_gcal_timezone(gcal_account,calendarid="primary"):
+    gcal = gcal_account.calendars().get(calendarId=calendarid).execute()
+    return exchangelib.EWSTimeZone.timezone(gcal['timeZone'])
+
+def main():
+    try:
+        with open(cachepath,"rb") as f:
+            cache = pickle.load(f)
+    except FileNotFoundError:
+        cache = None
+
+    ex_account = ex_login("mv3@sanger.ac.uk")
+    current,itemids = get_ex_events(ex_account.calendar)
+
+    gcal_account = gcal_login()
+    gcal_tz = get_gcal_timezone(gcal_account)
+    
+    if cache is not None:
+        added,deleted,changed = ex_event_changes(cache,current)
+        add_ex_to_gcal(ex_account,gcal_account,gcal_tz,current,
+                       itemids,added)
+        
+    with open(cachepath,"wb") as f:
+        pickle.dump(current,f)
+
+if __name__ == "__main__":
+    main()
+
+