@@ -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,108 @@ 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 (event ["organizer" ]["email" ] == options .zulip_email or event ["organizer" ]["self" ])
174+ else event ["organizer" ]["displayName" ]
175+ )
176+ hangout_link = event .get ("hangoutLink" , "" )
177+ events .append (
178+ {
179+ "id" : id ,
180+ "start" : start ,
181+ "end" : end ,
182+ "summary" : summary ,
183+ "html_link" : html_link ,
184+ "status" : status ,
185+ "location" : location ,
186+ "description" : description ,
187+ "organizer" : organizer ,
188+ "hangout_link" : hangout_link ,
189+ }
190+ )
149191
150- try :
151- events .append ((event ["id" ], start , event ["summary" ]))
152- except KeyError :
153- events .append ((event ["id" ], start , "(No Title)" ))
192+
193+ def event_to_message (event : Event ) -> str :
194+ """Parse the event dictionary and return a string that can be sent as a message.
195+
196+ The message includes the event title, start and end times, location, organizer, hangout link, and description.
197+
198+ Returns:
199+ str: The message to be sent.
200+ """
201+ line = f"**[{ event ['summary' ]} ]({ event ['html_link' ]} )**\n "
202+ if event ["start" ].hour == 0 and event ["start" ].minute == 0 :
203+ line += "Scheduled for today.\n "
204+ else :
205+ line += f"Scheduled from **{ event ['start' ].strftime ('%H:%M' )} ** to **{ event ['end' ].strftime ('%H:%M' )} **.\n "
206+ line += f"**Location:** { event ['location' ]} \n " if event ["location" ] else ""
207+ line += f"**Organizer:** { event ['organizer' ]} \n " if event ["organizer" ] else ""
208+ line += (
209+ f"**Hangout Link:** [{ event ['hangout_link' ].split ('/' )[2 ]} ]({ event ['hangout_link' ]} )\n "
210+ if event ["hangout_link" ]
211+ else ""
212+ )
213+ line += f"**Description:** { event ['description' ]} \n " if event ["description" ] else ""
214+ return line
154215
155216
156217def send_reminders () -> Optional [None ]:
157218 messages = []
158219 keys = set ()
159220 now = datetime .datetime .now (tz = pytz .utc )
160221
161- for id , start , summary in events :
162- dt = start - now
222+ for event in events :
223+ dt = event [ " start" ] - now
163224 if dt .days == 0 and dt .seconds < 60 * options .interval :
164225 # The unique key includes the start time, because of
165226 # repeating events.
166- key = (id , start )
227+ key = (event [ "id" ], event [ " start" ] )
167228 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" ))
229+ line = event_to_message (event )
172230 print ("Sending reminder:" , line )
173231 messages .append (line )
174232 keys .add (key )
175-
176233 if not messages :
177234 return
178235
179236 if len (messages ) == 1 :
180- message = "Reminder: " + messages [0 ]
237+ message = "** Reminder:** \n \n " + messages [0 ]
181238 else :
182- message = "Reminder:\n \n " + "\n " .join ("* " + m for m in messages )
239+ message = "**Reminders:**\n \n " + "\n " .join (
240+ str (i + 1 ) + ". " + m for i , m in enumerate (messages )
241+ )
183242
184243 zulip_client .send_message (dict (type = "private" , to = options .zulip_email , content = message ))
185244
0 commit comments