chiark / gitweb /
7848916d0893bfecdabe47bcbf124ab95c909f9a
[gooswapper] / gooswapper.py
1 #!/usr/bin/env python3
2
3 import sys
4 import getpass
5 import os
6 import pickle
7 import logging
8 logger = logging.getLogger('gooswapper')
9 logger.setLevel(logging.INFO)
10 consolelog = logging.StreamHandler()
11 consolelog.setLevel(logging.INFO)
12 logformatter = logging.Formatter('%(name)s - %(levelname)s - %(message)s')
13 consolelog.setFormatter(logformatter)
14 logger.addHandler(consolelog)
15 #We can't use this, because that way all the libraries' logs spam us
16 #logging.basicConfig(level=logging.INFO)
17
18 #Exchange-related library
19 sys.path.append("/upstreams/exchangelib")
20 import exchangelib
21
22 #Google calendar-api libraries
23 import httplib2
24 import apiclient.discovery
25 import oauth2client
26 import oauth2client.file
27 import oauth2client.client
28
29 #Not sure what the distribution approach here is...
30 gcal_client_id = '805127902516-ptbbtgpq9o8pjr6r3k6hsm60j589o85u.apps.googleusercontent.com'
31 gcal_client_secret = '8hpdxV3MauorryTDoZ1YK8JO'
32
33 #scope URL for r/w calendar access
34 scope = 'https://www.googleapis.com/auth/calendar'
35 #flow object, for doing OAuth2.0 stuff
36 flow = oauth2client.client.OAuth2WebServerFlow(gcal_client_id,
37                                                gcal_client_secret,
38                                                scope)
39
40
41 gcal_authpath=".gooswap_gcal_creds.dat"
42
43 cachepath=".gooswapcache"
44
45 exchange_credential = None
46
47 class ex_gcal_link(exchangelib.ExtendedProperty):
48     distinguished_property_set_id = 'PublicStrings'
49     #property_set_id = '12345678-1234-1234-1234-123456781234'
50     #    property_set_id = "gooswapper"
51     property_name = "google calendar event id"
52     property_type = 'String'
53
54 exchangelib.CalendarItem.register('gcal_link',ex_gcal_link)
55
56 #see docs for exchangelib.UID for why this is needed
57 class GlobalObjectId(exchangelib.ExtendedProperty):
58      distinguished_property_set_id = 'Meeting'
59      property_id = 3
60      property_type = 'Binary'
61
62 exchangelib.CalendarItem.register('global_object_id', GlobalObjectId)
63
64 def get_ex_event_by_uid(calendar,uid):
65     return calendar.get(global_object_id=GlobalObjectId(exchangelib.UID(uid)))
66
67 def get_ex_event_by_id_and_changekey(acct,itemid,changekey):
68     l=list(acct.fetch([(itemid,changekey)]))
69     return list(acct.fetch([(itemid,changekey)]))[0]
70
71 def get_ex_cred(username="SANGER\mv3",password=None):
72     if password is None:
73         password = getpass.getpass(prompt="Password for user %s: " % username)
74     return exchangelib.ServiceAccount(username,password)
75 #    return exchangelib.Credentials(username,password)
76
77 def ex_login(emailaddr,autodiscover=True):
78     global exchange_credential
79     if exchange_credential is None:
80         exchange_credential = get_ex_cred()
81     return exchangelib.Account(emailaddr,
82                                credentials = exchange_credential,
83                                autodiscover = autodiscover)
84
85 def get_ex_events(calendar):
86     ans={}
87     itemids={}
88     for event in calendar.all().only('uid','changekey','item_id','gcal_link'):
89         if event.gcal_link is not None:
90 #            event.delete()
91             continue
92         if event.uid in ans:
93             logger.warning("Event uid %s was duplicated!" % event.uid)
94         ans[event.uid] = event.changekey
95         itemids[event.uid] = event.item_id
96     logger.info("%d events found" % len(ans))
97     return (ans,itemids)
98
99 def ex_event_changes(old,new):
100     olds = set(old.keys())
101     news = set(new.keys())
102     added = list(news-olds)
103     deleted = list(olds-news)
104     changed = []
105     #intersection - i.e. common to both sets
106     for event in olds & news:
107         if old[event] != new[event]:
108             changed.append(event)
109     logger.info("%d events updated, %d added, %d deleted" % (len(changed),
110                                                               len(added),
111                                                               len(deleted)))
112     return added, deleted, changed
113
114 #XXX doesn't work - cf https://github.com/ecederstrand/exchangelib/issues/492
115 def add_ex_to_gcal_needs_ev_id(ex_cal,events):
116     for ev_id in events:
117         print(ev_id)
118         event = get_ex_event_by_uid(ex_cal,ev_id)
119         event.gcal_link = "Testing"
120         event.save()
121
122 def add_ex_to_gcal(ex_acct,
123                    gcal_acct,gcal_tz,events,
124                    itemids,added,
125                    gcal_id="primary"):
126     for ev_id in added:
127         event = get_ex_event_by_id_and_changekey(ex_acct,
128                                                  itemids[ev_id],events[ev_id])
129         if event.is_all_day:
130             gevent={}
131             gevent["summary"]=event.subject
132             gevent["end"]={"date": str(event.end.astimezone(gcal_tz).date())}
133             gevent["start"]={"date": str(event.start.astimezone(gcal_tz).date())}
134             if event.text_body.strip() != '':
135                 gevent["description"] = event.text_body
136             if event.location is not None:
137                 gevent["location"] = event.location
138             gevent["extended_properties"]={"shared": {"ex_id": event.item_id}}
139             gevent=gcal_acct.events().insert(calendarId=gcal_id, body=gevent).execute()
140             event.gcal_link = gevent.get("id")
141             event.save()
142             events[event.uid] = event.changekey
143         else:
144             logger.warning("only all-day events supported")
145             
146 def get_gcal_cred():
147     #each such file can only store a single credential
148     storage = oauth2client.file.Storage(gcal_authpath)
149     gcal_credential = storage.get()
150     #if no credential found, or they're invalid (e.g. expired),
151     #then get a new one; pass --noauth_local_webserver on the command line
152     #if you don't want it to spawn a browser
153     if gcal_credential is None or gcal_credential.invalid:
154         gcal_credential = oauth2client.tools.run_flow(flow,
155                                                       storage,
156                                                       oauth2client.tools.argparser.parse_args())
157     return gcal_credential
158
159 def gcal_login():
160     gcal_credential = get_gcal_cred()
161     # Object to handle http requests; could add proxy details
162     http = httplib2.Http()
163     http = gcal_credential.authorize(http)
164     return apiclient.discovery.build('calendar', 'v3', http=http)
165
166 def get_gcal_timezone(gcal_account,calendarid="primary"):
167     gcal = gcal_account.calendars().get(calendarId=calendarid).execute()
168     return exchangelib.EWSTimeZone.timezone(gcal['timeZone'])
169
170 def main():
171     try:
172         with open(cachepath,"rb") as f:
173             cache = pickle.load(f)
174     except FileNotFoundError:
175         cache = None
176
177     ex_account = ex_login("mv3@sanger.ac.uk")
178     current,itemids = get_ex_events(ex_account.calendar)
179
180     gcal_account = gcal_login()
181     gcal_tz = get_gcal_timezone(gcal_account)
182     
183     if cache is not None:
184         added,deleted,changed = ex_event_changes(cache,current)
185         add_ex_to_gcal(ex_account,gcal_account,gcal_tz,current,
186                        itemids,added)
187         
188     with open(cachepath,"wb") as f:
189         pickle.dump(current,f)
190
191 if __name__ == "__main__":
192     main()
193
194