From: Matthew Vernon Date: Fri, 5 Oct 2018 08:31:07 +0000 (+0100) Subject: Initial commit X-Git-Tag: v0.1~40 X-Git-Url: https://www.chiark.greenend.org.uk/ucgi/~matthewv/git?a=commitdiff_plain;h=09918df08c1f22cf0782910f4f0c7e94f6de01fd;p=gooswapper Initial commit This now can cope with new all-day events from exchange, and make the corresponding event in gcal. --- diff --git a/gooswapper.py b/gooswapper.py new file mode 100644 index 0000000..7848916 --- /dev/null +++ b/gooswapper.py @@ -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() + +