--- /dev/null
+#!/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()
+
+