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