@@ -9,7 +9,7 @@ import logging
99import os
1010import sys
1111import time
12- from typing import List , Optional , Set , Tuple
12+ from typing import List , Optional , Set , Tuple , TypedDict
1313
1414import dateutil .parser
1515import pytz
@@ -28,8 +28,22 @@ CLIENT_SECRET_FILE = "client_secret.json" # noqa: S105
2828APPLICATION_NAME = "Zulip"
2929HOME_DIR = os .path .expanduser ("~" )
3030
31+
32+ class Event (TypedDict ):
33+ id : int
34+ start : datetime .datetime
35+ end : datetime .datetime
36+ summary : str
37+ html_link : str
38+ status : str
39+ location : str
40+ description : str
41+ organizer : str
42+ hangout_link : str
43+
44+
3145# Our cached view of the calendar, updated periodically.
32- events : List [Tuple [ int , datetime . datetime , str ] ] = []
46+ events : List [Event ] = []
3347
3448# Unique keys for events we've already sent, so we don't remind twice.
3549sent : Set [Tuple [int , datetime .datetime ]] = set ()
@@ -123,63 +137,111 @@ def populate_events() -> Optional[None]:
123137 )
124138 .execute ()
125139 )
126-
127140 events .clear ()
128141 for event in feed ["items" ]:
129142 try :
130143 start = dateutil .parser .parse (event ["start" ]["dateTime" ])
144+ end = dateutil .parser .parse (event ["end" ]["dateTime" ])
131145 # According to the API documentation, a time zone offset is required
132146 # for start.dateTime unless a time zone is explicitly specified in
133147 # start.timeZone.
134- if start .tzinfo is None :
148+ if start .tzinfo is None or end . tzinfo is None :
135149 event_timezone = pytz .timezone (event ["start" ]["timeZone" ])
136150 # pytz timezones include an extra localize method that's not part
137151 # of the tzinfo base class.
138152 start = event_timezone .localize (start )
153+ end = event_timezone .localize (end )
139154 except KeyError :
140155 # All-day events can have only a date.
141156 start_naive = dateutil .parser .parse (event ["start" ]["date" ])
142-
157+ end_naive = dateutil . parser . parse ( event [ "end" ][ "date" ])
143158 # All-day events don't have a time zone offset; instead, we use the
144159 # time zone of the calendar.
145160 calendar_timezone = pytz .timezone (feed ["timeZone" ])
146161 # pytz timezones include an extra localize method that's not part
147162 # of the tzinfo base class.
148163 start = calendar_timezone .localize (start_naive )
164+ end = calendar_timezone .localize (end_naive )
165+ id = event ["id" ]
166+ summary = event .get ("summary" , "(No Title)" )
167+ html_link = event ["htmlLink" ]
168+ status = event .get ("status" , "confirmed" )
169+ location = event .get ("location" , "" )
170+ description = event .get ("description" , "" )
171+ organizer = (
172+ ""
173+ if (
174+ event ["organizer" ]["email" ] == options .zulip_email or event ["organizer" ].get ("self" )
175+ )
176+ else event ["organizer" ].get ("displayName" , event ["organizer" ]["email" ])
177+ )
178+ hangout_link = event .get ("hangoutLink" , "" )
179+ events .append (
180+ {
181+ "id" : id ,
182+ "start" : start ,
183+ "end" : end ,
184+ "summary" : summary ,
185+ "html_link" : html_link ,
186+ "status" : status ,
187+ "location" : location ,
188+ "description" : description ,
189+ "organizer" : organizer ,
190+ "hangout_link" : hangout_link ,
191+ }
192+ )
149193
150- try :
151- events .append ((event ["id" ], start , event ["summary" ]))
152- except KeyError :
153- events .append ((event ["id" ], start , "(No Title)" ))
194+
195+ def event_to_message (event : Event ) -> str :
196+ """Parse the event dictionary and return a string that can be sent as a message.
197+
198+ The message includes the event title, start and end times, location, organizer, hangout link, and description.
199+
200+ Returns:
201+ str: The message to be sent.
202+ """
203+ line = f"**[{ event ['summary' ]} ]({ event ['html_link' ]} )**\n "
204+ if event ["start" ].hour == 0 and event ["start" ].minute == 0 :
205+ line += "Scheduled for today.\n "
206+ else :
207+ line += f"Scheduled from **{ event ['start' ].strftime ('%H:%M' )} ** to **{ event ['end' ].strftime ('%H:%M' )} **.\n "
208+ line += f"**Location:** { event ['location' ]} \n " if event ["location" ] else ""
209+ line += f"**Organizer:** { event ['organizer' ]} \n " if event ["organizer" ] else ""
210+ line += (
211+ f"**Hangout Link:** [{ event ['hangout_link' ].split ('/' )[2 ]} ]({ event ['hangout_link' ]} )\n "
212+ if event ["hangout_link" ]
213+ else ""
214+ )
215+ line += f"**Status:** { event ['status' ]} \n " if event ["status" ] else ""
216+ line += f"**Description:** { event ['description' ]} \n " if event ["description" ] else ""
217+ return line
154218
155219
156220def send_reminders () -> Optional [None ]:
157221 messages = []
158222 keys = set ()
159223 now = datetime .datetime .now (tz = pytz .utc )
160224
161- for id , start , summary in events :
162- dt = start - now
225+ for event in events :
226+ dt = event [ " start" ] - now
163227 if dt .days == 0 and dt .seconds < 60 * options .interval :
164228 # The unique key includes the start time, because of
165229 # repeating events.
166- key = (id , start )
230+ key = (event [ "id" ], event [ " start" ] )
167231 if key not in sent :
168- if start .hour == 0 and start .minute == 0 :
169- line = f"{ summary } is today."
170- else :
171- line = "{} starts at {}" .format (summary , start .strftime ("%H:%M" ))
232+ line = event_to_message (event )
172233 print ("Sending reminder:" , line )
173234 messages .append (line )
174235 keys .add (key )
175-
176236 if not messages :
177237 return
178238
179239 if len (messages ) == 1 :
180- message = "Reminder: " + messages [0 ]
240+ message = "** Reminder:** \n \n " + messages [0 ]
181241 else :
182- message = "Reminder:\n \n " + "\n " .join ("* " + m for m in messages )
242+ message = "**Reminders:**\n \n " + "\n " .join (
243+ str (i + 1 ) + ". " + m for i , m in enumerate (messages )
244+ )
183245
184246 zulip_client .send_message (dict (type = "private" , to = options .zulip_email , content = message ))
185247
0 commit comments