@@ -9,7 +9,7 @@ import logging
9
9
import os
10
10
import sys
11
11
import time
12
- from typing import List , Optional , Set , Tuple
12
+ from typing import List , Optional , Set , Tuple , TypedDict
13
13
14
14
import dateutil .parser
15
15
import pytz
@@ -28,8 +28,22 @@ CLIENT_SECRET_FILE = "client_secret.json" # noqa: S105
28
28
APPLICATION_NAME = "Zulip"
29
29
HOME_DIR = os .path .expanduser ("~" )
30
30
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
+
31
45
# Our cached view of the calendar, updated periodically.
32
- events : List [Tuple [ int , datetime . datetime , str ] ] = []
46
+ events : List [Event ] = []
33
47
34
48
# Unique keys for events we've already sent, so we don't remind twice.
35
49
sent : Set [Tuple [int , datetime .datetime ]] = set ()
@@ -123,63 +137,108 @@ def populate_events() -> Optional[None]:
123
137
)
124
138
.execute ()
125
139
)
126
-
127
140
events .clear ()
128
141
for event in feed ["items" ]:
129
142
try :
130
143
start = dateutil .parser .parse (event ["start" ]["dateTime" ])
144
+ end = dateutil .parser .parse (event ["end" ]["dateTime" ])
131
145
# According to the API documentation, a time zone offset is required
132
146
# for start.dateTime unless a time zone is explicitly specified in
133
147
# start.timeZone.
134
- if start .tzinfo is None :
148
+ if start .tzinfo is None or end . tzinfo is None :
135
149
event_timezone = pytz .timezone (event ["start" ]["timeZone" ])
136
150
# pytz timezones include an extra localize method that's not part
137
151
# of the tzinfo base class.
138
152
start = event_timezone .localize (start )
153
+ end = event_timezone .localize (end )
139
154
except KeyError :
140
155
# All-day events can have only a date.
141
156
start_naive = dateutil .parser .parse (event ["start" ]["date" ])
142
-
157
+ end_naive = dateutil . parser . parse ( event [ "end" ][ "date" ])
143
158
# All-day events don't have a time zone offset; instead, we use the
144
159
# time zone of the calendar.
145
160
calendar_timezone = pytz .timezone (feed ["timeZone" ])
146
161
# pytz timezones include an extra localize method that's not part
147
162
# of the tzinfo base class.
148
163
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
+ )
149
191
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
154
215
155
216
156
217
def send_reminders () -> Optional [None ]:
157
218
messages = []
158
219
keys = set ()
159
220
now = datetime .datetime .now (tz = pytz .utc )
160
221
161
- for id , start , summary in events :
162
- dt = start - now
222
+ for event in events :
223
+ dt = event [ " start" ] - now
163
224
if dt .days == 0 and dt .seconds < 60 * options .interval :
164
225
# The unique key includes the start time, because of
165
226
# repeating events.
166
- key = (id , start )
227
+ key = (event [ "id" ], event [ " start" ] )
167
228
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 )
172
230
print ("Sending reminder:" , line )
173
231
messages .append (line )
174
232
keys .add (key )
175
-
176
233
if not messages :
177
234
return
178
235
179
236
if len (messages ) == 1 :
180
- message = "Reminder: " + messages [0 ]
237
+ message = "** Reminder:** \n \n " + messages [0 ]
181
238
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
+ )
183
242
184
243
zulip_client .send_message (dict (type = "private" , to = options .zulip_email , content = message ))
185
244
0 commit comments