Various changes
[kiosk.git] / gcal_renderer.py
1 from oauth2client.client import AccessTokenRefreshError
2 import constants
3 import datetime
4 import file_writer
5 import gdata
6 import globals
7 import os
8 import renderer
9 import time
10
11 class gcal_renderer(renderer.debuggable_abstaining_renderer):
12     """A renderer to fetch upcoming events from www.google.com/calendar"""
13
14     calendar_whitelist = frozenset([
15         'Alex\'s calendar',
16         'Family',
17         'Holidays in United States',
18         'Lynn Gasch',
19         'Lynn\'s Work',
20         '[email protected]',
21         'Scott Gasch External - Misc',
22         'Birthdays',  # <-- from g+ contacts
23     ])
24
25     class comparable_event(object):
26         """A helper class to sort events."""
27         def __init__(self, start_time, end_time, summary, calendar):
28             if start_time is None:
29                 assert(end_time is None)
30             self.start_time = start_time
31             self.end_time = end_time
32             self.summary = summary
33             self.calendar = calendar
34
35         def __lt__(self, that):
36             if self.start_time is None and that.start_time is None:
37                 return self.summary < that.summary
38             if self.start_time is None or that.start_time is None:
39                 return self.start_time is None
40             return (self.start_time,
41                     self.end_time,
42                     self.summary,
43                     self.calendar) < (that.start_time,
44                                       that.end_time,
45                                       that.summary,
46                                       that.calendar)
47
48         def __str__(self):
49             return '[%s]&nbsp;%s' % (self.timestamp(), self.friendly_name())
50
51         def friendly_name(self):
52             name = self.summary
53             name = name.replace("countdown:", "")
54             return "<B>%s</B>" % name
55
56         def timestamp(self):
57             if self.start_time is None:
58                 return "None"
59             elif (self.start_time.hour == 0):
60                 return datetime.datetime.strftime(self.start_time,
61                                                    '%a %b %d %Y')
62             else:
63                 return datetime.datetime.strftime(self.start_time,
64                                                   '%a %b %d %Y %H:%M%p')
65
66     def __init__(self, name_to_timeout_dict, oauth):
67         super(gcal_renderer, self).__init__(name_to_timeout_dict, True)
68         self.oauth = oauth
69         self.client = self.oauth.calendar_service()
70         self.sortable_events = []
71         self.countdown_events = []
72
73     def debug_prefix(self):
74         return "gcal"
75
76     def periodic_render(self, key):
77         self.debug_print('called for "%s"' % key)
78         if (key == "Render Upcoming Events"):
79             return self.render_upcoming_events()
80         elif (key == "Look For Triggered Events"):
81             return self.look_for_triggered_events()
82         else:
83             raise error('Unexpected operation')
84
85     def render_upcoming_events(self):
86         page_token = None
87         def format_datetime(x):
88             return datetime.datetime.strftime(x, '%Y-%m-%dT%H:%M:%SZ')
89         now = datetime.datetime.now()
90         time_min = now - datetime.timedelta(1)
91         time_max = now + datetime.timedelta(95)
92         time_min, time_max = list(map(format_datetime, (time_min, time_max)))
93         self.debug_print("time_min is %s" % time_min)
94         self.debug_print("time_max is %s" % time_max)
95
96         # Writes 2 files:
97         #  + "upcoming events",
98         #  + a countdown timer for a subser of events,
99         f = file_writer.file_writer('gcal_3_86400.html')
100         f.write('<h1>Upcoming Calendar Events:</h1><hr>\n')
101         f.write('<center><table width=96%>\n')
102
103         g = file_writer.file_writer('countdown_3_7200.html')
104         g.write('<h1>Countdowns:</h1><hr><ul>\n')
105
106         try:
107             self.sortable_events = []
108             self.countdown_events = []
109             while True:
110                 calendar_list = self.client.calendarList().list(
111                     pageToken=page_token).execute()
112                 for calendar in calendar_list['items']:
113                     if (calendar['summary'] in gcal_renderer.calendar_whitelist):
114                         events = self.client.events().list(
115                             calendarId=calendar['id'],
116                             singleEvents=True,
117                             timeMin=time_min,
118                             timeMax=time_max,
119                             maxResults=50).execute()
120
121                         def parse_date(x):
122                             y = x.get('date')
123                             if y:
124                                 y = datetime.datetime.strptime(y, '%Y-%m-%d')
125                             else:
126                                 y = x.get('dateTime')
127                                 if y:
128                                     y = datetime.datetime.strptime(y[:-6],
129                                                          '%Y-%m-%dT%H:%M:%S')
130                                 else:
131                                     y = None
132                             return y
133
134                         for event in events['items']:
135                             try:
136                                 summary = event['summary']
137                                 self.debug_print("event '%s' (%s to %s)" % (
138                                     summary, event['start'], event['end']))
139                                 start = parse_date(event['start'])
140                                 end = parse_date(event['end'])
141                                 self.sortable_events.append(
142                                     gcal_renderer.comparable_event(start,
143                                                                    end,
144                                                                    summary,
145                                                                    calendar['summary']))
146                                 if ('countdown' in summary or
147                                     'Holidays' in calendar['summary'] or
148                                     'Countdown' in summary):
149                                     self.debug_print("event is countdown worthy")
150                                     self.countdown_events.append(
151                                         gcal_renderer.comparable_event(start,
152                                                                        end,
153                                                                        summary,
154                                                                        calendar['summary']))
155                             except Exception as e:
156                                 print("gcal unknown exception, skipping event.");
157                     else:
158                         self.debug_print("Skipping calendar '%s'" % calendar['summary'])
159                 page_token = calendar_list.get('nextPageToken')
160                 if not page_token: break
161
162             self.sortable_events.sort()
163             upcoming_sortable_events = self.sortable_events[:12]
164             for event in upcoming_sortable_events:
165                 self.debug_print("sorted event: %s" % event.friendly_name())
166                 f.write("""
167 <tr>
168   <td style="padding-right: 1em;">
169     %s
170   </td>
171   <td style="padding-left: 1em;">
172     %s
173   </td>
174 </tr>\n""" % (event.timestamp(), event.friendly_name()))
175             f.write('</table></center>\n')
176             f.close()
177
178             self.countdown_events.sort()
179             upcoming_countdown_events = self.countdown_events[:12]
180             now = datetime.datetime.now()
181             count = 0
182             timestamps = { }
183             for event in upcoming_countdown_events:
184                 eventstamp = event.start_time
185                 delta = eventstamp - now
186                 name = event.friendly_name()
187                 x = int(delta.total_seconds())
188                 if x > 0:
189                     identifier = "id%d" % count
190                     days = divmod(x, constants.seconds_per_day)
191                     hours = divmod(days[1], constants.seconds_per_hour)
192                     minutes = divmod(hours[1], constants.seconds_per_minute)
193                     g.write('<li><SPAN id="%s">%d days, %02d:%02d</SPAN> until %s</li>\n' % (identifier, days[0], hours[0], minutes[0], name))
194                     timestamps[identifier] = time.mktime(eventstamp.timetuple())
195                     count += 1
196                     self.debug_print("countdown to %s is %dd %dh %dm" % (
197                         name, days[0], hours[0], minutes[0]))
198             g.write('</ul>')
199             g.write('<SCRIPT>\nlet timestampMap = new Map([')
200             for x in list(timestamps.keys()):
201                 g.write('    ["%s", %f],\n' % (x, timestamps[x] * 1000.0))
202             g.write(']);\n\n')
203             g.write("""
204 // Pad things with a leading zero if necessary.
205 function pad(n) {
206     return (n < 10) ? ("0" + n) : n;
207 }
208
209 // Return an 's' if things are plural.
210 function plural(n) {
211     return (n == 1) ? "" : "s";
212 }
213
214 // Periodic function to run the page timers.
215 var fn = setInterval(function() {
216     var now = new Date().getTime();
217     for (let [id, timestamp] of timestampMap) {
218         var delta = timestamp - now;
219
220         if (delta > 0) {
221             var days = Math.floor(delta / (1000 * 60 * 60 * 24));
222             var hours = pad(Math.floor((delta % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)));
223             var minutes = pad(Math.floor((delta % (1000 * 60 * 60)) / (1000 * 60)));
224             var seconds = pad(Math.floor((delta % (1000 * 60)) / 1000));
225
226             var s = days + " day" + plural(days) + ", ";
227             s = s + hours + ":" + minutes;
228             document.getElementById(id).innerHTML = s;
229         } else {
230             document.getElementById(id).innerHTML = "EXPIRED";
231         }
232     }
233 }, 1000);
234 </script>""");
235             g.close()
236             return True
237         except (gdata.service.RequestError, AccessTokenRefreshError):
238             print("********* TRYING TO REFRESH GCAL CLIENT *********")
239             self.oauth.refresh_token()
240             self.client = self.oauth.calendar_service()
241             return False
242         except:
243             raise
244
245     def look_for_triggered_events(self):
246         f = file_writer.file_writer(constants.gcal_imminent_pagename)
247         f.write('<h1>Imminent Upcoming Calendar Events:</h1>\n<hr>\n')
248         f.write('<center><table width=99%>\n')
249         now = datetime.datetime.now()
250         count = 0
251         for event in self.sortable_events:
252             eventstamp = event.start_time
253             delta = eventstamp - now
254             x = int(delta.total_seconds())
255             if x > 0 and x <= constants.seconds_per_minute * 3:
256                 days = divmod(x, constants.seconds_per_day)
257                 hours = divmod(days[1], constants.seconds_per_hour)
258                 minutes = divmod(hours[1], constants.seconds_per_minute)
259                 eventstamp = event.start_time
260                 name = event.friendly_name()
261                 calendar = event.calendar
262                 f.write("<LI> %s (%s) upcoming in %d minutes.\n" % (name, calendar, minutes[0]))
263                 count += 1
264         f.write("</table>")
265         f.close()
266         if count > 0:
267             globals.put("gcal_triggered", True)
268         else:
269             globals.put("gcal_triggered", False)
270         return True