chiark / gitweb /
Use start timezone for finding recurrences, and date only for all-days
[gooswapper] / gooswapper.py
index 41dd8075abcd20ba04c6dd44cb74564e1d963751..a9da3680a7b86c095193aa641ef4bb87129d3ecc 100644 (file)
@@ -1,11 +1,30 @@
 #!/usr/bin/env python3
 
+# Copyright (C) 2018 Genome Research Limited
+#
+# Author: Matthew Vernon <mv3@sanger.ac.uk>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
 import sys
 import getpass
 import os
+import os.path
 import pickle
 import collections
 import argparse
+import time
 import logging
 logger = logging.getLogger('gooswapper')
 logger.setLevel(logging.INFO)
@@ -40,10 +59,10 @@ flow = oauth2client.client.OAuth2WebServerFlow(gcal_client_id,
                                                gcal_client_secret,
                                                scope)
 
+gsdir = os.path.expanduser("~/.gooswapper")
+gcal_authpath = gsdir + "/.gooswap_gcal_creds.dat"
 
-gcal_authpath=".gooswap_gcal_creds.dat"
-
-cachepath=".gooswapcache"
+cachepath=None
 
 exchange_credential = None
 
@@ -78,13 +97,18 @@ def get_ex_event_by_id_and_changekey(acct,itemid,changekey):
 def get_gcal_event_by_eventid(gcal_acct,eventId,gcal_id="primary"):
     return gcal_acct.events().get(calendarId=gcal_id,eventId=eventId).execute()
 
-def get_gcal_recur_instance(gcal_acct,gcal_master,start,gcal_id="primary"):
+def get_gcal_recur_instance(gcal_acct,gcal_master,start,is_all_day,stz,
+                            gcal_id="primary"):
     if gcal_master is None:
         logger.warning("Cannot get recurrences from event with null gcal id")
         return None
+    if is_all_day:
+        os = str(start.astimezone(stz).date())
+    else:
+        os = start.astimezone(stz).isoformat()
     ans = gcal_acct.events().instances(calendarId=gcal_id,
                                        eventId=gcal_master,
-                                       originalStart=start.isoformat(),
+                                       originalStart=os,
                                        showDeleted=True).execute()
     if len(ans['items']) != 1:
         logger.error("Searching for recurrance instance returned %d events" % \
@@ -224,7 +248,10 @@ def modify_recurring(ex_acct,gcal_acct,gcal_tz,
     if master.modified_occurrences is not None:
         for mod in master.modified_occurrences:
             instance = get_gcal_recur_instance(gcal_acct,master.gcal_link,
-                                               mod.original_start,gcal_id)
+                                               mod.original_start,
+                                               master.is_all_day,
+                                               master._start_timezone,
+                                               gcal_id)
             if instance is None: #give up after first failure
                 return
             mod_event = get_ex_event_by_itemid(ex_acct.calendar,mod.item_id)
@@ -238,7 +265,10 @@ def modify_recurring(ex_acct,gcal_acct,gcal_tz,
     if master.deleted_occurrences is not None:
         for d in master.deleted_occurrences:
             instance = get_gcal_recur_instance(gcal_acct,master.gcal_link,
-                                               d.start,gcal_id)
+                                               d.start,
+                                               master.is_all_day,
+                                               master._start_timezone,
+                                               gcal_id)
             if instance is None: #give up after any failure
                 return
             if instance["status"] != "cancelled":
@@ -251,14 +281,24 @@ def modify_recurring(ex_acct,gcal_acct,gcal_tz,
 def build_gcal_event_from_ex(event,gcal_tz):
     gevent={}
     gevent["summary"]=event.subject
+    #Use the event's timezones if possible, otherwise fall back to the
+    #target calendar timezone
+    if event._start_timezone is not None:
+        stz = event._start_timezone
+    else:
+        stz = gcal_tz
+    if event._end_timezone is not None:
+        etz = event._end_timezone
+    else:
+        etz = gcal_tz
     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["end"]={"date": str(event.end.astimezone(etz).date())}
+        gevent["start"]={"date": str(event.start.astimezone(stz).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)}
+        gevent["end"]={"dateTime": event.end.astimezone(etz).isoformat(),
+                       "timeZone": str(etz)}
+        gevent["start"]={"dateTime": event.start.astimezone(stz).isoformat(),
+                         "timeZone": str(stz)}
     if event.text_body is not None and event.text_body.strip() != '':
         gevent["description"] = event.text_body
     if event.location is not None:
@@ -418,34 +458,54 @@ def main():
         gcal_id = "primary"
     else:
         gcal_id = args.gcalid
-    try:
-        with open(cachepath,"rb") as f:
-            cache = pickle.load(f)
-    except FileNotFoundError:
-        cache = None
 
-    ex_account = ex_login(args.exchuser,args.exchemail,
-                          ".gooswapper_exch_conf.dat")
-    current = get_ex_events(ex_account.calendar)
+    #Make our config dir if it doesn't exist
+    if not os.path.exists(gsdir):
+        os.mkdir(gsdir,0o700)
+    #Cache file is specific to the Exchange calendar
+    global cachepath
+    cachepath = gsdir + "/.cache-%s" % \
+                (args.exchemail.replace('@','_').replace('/','_'))
 
+    #log in to the accounts
+    ex_account = ex_login(args.exchuser,args.exchemail,
+                          gsdir+"/.gooswapper_exch_conf.dat")
     gcal_account = gcal_login(args)
     gcal_tz = get_gcal_timezone(gcal_account,gcal_id)
-    
-    if cache is not None:
-        added,deleted,changed = ex_event_changes(cache,current)
-        add_ex_to_gcal(ex_account,gcal_account,gcal_tz,current,added,gcal_id)
-        #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,gcal_id)
-        update_ex_to_gcal(ex_account,gcal_account,gcal_tz,current,
-                          changed,gcal_id)
-    else:
-        toadd = match_ex_to_gcal(ex_account,gcal_account,gcal_tz,current,
-                                 gcal_id)
-        add_ex_to_gcal(ex_account,gcal_account,gcal_tz,current,toadd,gcal_id)
+
+    #Main loop (broken at the end if login is false)
+    while True:
+        try:
+            with open(cachepath,"rb") as f:
+                cache = pickle.load(f)
+        except FileNotFoundError:
+            cache = None
+
+        current = get_ex_events(ex_account.calendar)
+
+        if cache is not None:
+            added,deleted,changed = ex_event_changes(cache,current)
+            add_ex_to_gcal(ex_account,gcal_account,gcal_tz,current,
+                           added,gcal_id)
+            #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,gcal_id)
+            update_ex_to_gcal(ex_account,gcal_account,gcal_tz,current,
+                              changed,gcal_id)
+        else:
+            toadd = match_ex_to_gcal(ex_account,gcal_account,gcal_tz,current,
+                                     gcal_id)
+            add_ex_to_gcal(ex_account,gcal_account,gcal_tz,current,
+                           toadd,gcal_id)
         
-    with open(cachepath,"wb") as f:
-        pickle.dump(current,f)
+        with open(cachepath,"wb") as f:
+            pickle.dump(current,f)
+
+        #If not looping, break here (after 1 run)
+        if args.loop==False:
+            break
+        #otherwise, wait 10 minutes, then go round again
+        time.sleep(600)
 
 if __name__ == "__main__":
     main()