1 #!/usr/bin/env python 2 3 """ 4 Handlers for a resource. 5 """ 6 7 from datetime import date, datetime, timedelta 8 from imiptools.content import Handler, format_datetime, get_value, parse_object 9 from imiptools.period import insert_period, period_overlaps, remove_period 10 from vCalendar import to_node 11 from vRecurrence import get_parameters, get_rule 12 13 class Event(Handler): 14 15 "An event handler." 16 17 def add(self): 18 pass 19 20 def cancel(self): 21 pass 22 23 def counter(self): 24 25 "Since this handler does not send requests, it will not handle replies." 26 27 pass 28 29 def declinecounter(self): 30 31 """ 32 Since this handler does not send counter proposals, it will not handle 33 replies to such proposals. 34 """ 35 36 pass 37 38 def publish(self): 39 pass 40 41 def refresh(self): 42 pass 43 44 def reply(self): 45 46 "Since this handler does not send requests, it will not handle replies." 47 48 pass 49 50 def request(self): 51 52 """ 53 Respond to a request by preparing a reply containing accept/decline 54 information for each indicated attendee. 55 56 No support for countering requests is implemented. 57 """ 58 59 oa = self.require_organiser_and_attendees() 60 if not oa: 61 return None 62 63 (organiser, organiser_attr), attendees = oa 64 65 # Process each attendee separately. 66 67 calendar = [] 68 69 for attendee, attendee_attr in attendees.items(): 70 71 # Check for event using UID. 72 73 f = self.store.get_event(attendee, self.uid) 74 event = f and parse_object(f, "utf-8", "VEVENT") 75 76 # If found, compare SEQUENCE and potentially DTSTAMP. 77 78 if event: 79 sequence = get_value(event, "SEQUENCE") 80 dtstamp = get_value(event, "DTSTAMP") 81 82 # If the request refers to an older version of the event, ignore 83 # it. 84 85 old_dtstamp = self.dtstamp < dtstamp 86 87 if sequence is not None and ( 88 int(self.sequence) < int(sequence) or 89 int(self.sequence) == int(sequence) and old_dtstamp 90 ) or old_dtstamp: 91 92 continue 93 94 # If newer than any old version, discard old details from the 95 # free/busy record and check for suitability. 96 97 dtstart = self.get_utc_datetime("DTSTART") 98 dtend = self.get_utc_datetime("DTEND") 99 100 # NOTE: Need also DURATION support. 101 102 duration = dtend - dtstart 103 104 # Recurrence rules create multiple instances to be checked. 105 # Conflicts may only be assessed within a period defined by policy 106 # for the agent, with instances outside that period being considered 107 # unchecked. 108 109 # NOTE: Need to expose the 100 day window in the configuration. 110 111 window_end = datetime.now() + timedelta(100) 112 113 # NOTE: Need also RDATE and EXDATE support. 114 115 rrule = self.get_value("RRULE") 116 117 if rrule: 118 selector = get_rule(dtstart, rrule) 119 parameters = get_parameters(rrule) 120 periods = [] 121 for start in selector.materialise(dtstart, window_end, parameters.get("COUNT"), parameters.get("BYSETPOS")): 122 start = datetime(*start, tzinfo=timezone("UTC")) 123 end = start + duration 124 periods.append((format_datetime(start), format_datetime(end))) 125 else: 126 periods = [(format_datetime(dtstart), format_datetime(dtend))] 127 128 conflict = False 129 freebusy = self.store.get_freebusy(attendee) 130 131 if freebusy: 132 remove_period(freebusy, self.uid) 133 conflict = True 134 for start, end in periods: 135 if period_overlaps(freebusy, (start, end)): 136 break 137 else: 138 conflict = False 139 else: 140 freebusy = [] 141 142 # If the event can be scheduled, it is registered and a reply sent 143 # accepting the event. (The attendee has PARTSTAT=ACCEPTED as an 144 # attribute.) 145 146 if not conflict: 147 for start, end in periods: 148 insert_period(freebusy, (start, end, self.uid)) 149 150 if self.get_value("TRANSP") in (None, "OPAQUE"): 151 self.store.set_freebusy(attendee, freebusy) 152 153 if self.publisher: 154 self.publisher.set_freebusy(attendee, freebusy) 155 156 self.store.set_event(attendee, self.uid, to_node( 157 {"VEVENT" : [(self.details, {})]} 158 )) 159 attendee_attr["PARTSTAT"] = "ACCEPTED" 160 161 # If the event cannot be scheduled, it is not registered and a reply 162 # sent declining the event. (The attendee has PARTSTAT=DECLINED as an 163 # attribute.) 164 165 else: 166 attendee_attr["PARTSTAT"] = "DECLINED" 167 168 self.details["ATTENDEE"] = [(attendee, attendee_attr)] 169 calendar.append(to_node( 170 {"VEVENT" : [(self.details, {})]} 171 )) 172 173 return calendar 174 175 class Freebusy(Handler): 176 177 "A free/busy handler." 178 179 def publish(self): 180 pass 181 182 def reply(self): 183 184 "Since this handler does not send requests, it will not handle replies." 185 186 pass 187 188 def request(self): 189 190 """ 191 Respond to a request by preparing a reply containing free/busy 192 information for each indicated attendee. 193 """ 194 195 oa = self.require_organiser_and_attendees() 196 if not oa: 197 return None 198 199 (organiser, organiser_attr), attendees = oa 200 201 # Construct an appropriate fragment. 202 203 calendar = [] 204 cwrite = calendar.append 205 206 # Get the details for each attendee. 207 208 for attendee, attendee_attr in attendees.items(): 209 freebusy = self.store.get_freebusy(attendee) 210 211 if freebusy: 212 record = [] 213 rwrite = record.append 214 215 rwrite(("ORGANIZER", organiser_attr, organiser)) 216 rwrite(("ATTENDEE", attendee_attr, attendee)) 217 rwrite(("UID", {}, self.uid)) 218 219 for start, end, uid in freebusy: 220 rwrite(("FREEBUSY", {"FBTYPE" : "BUSY"}, [start, end])) 221 222 cwrite(("VFREEBUSY", {}, record)) 223 224 # Return the reply. 225 226 return calendar 227 228 class Journal(Handler): 229 230 "A journal entry handler." 231 232 def add(self): 233 pass 234 235 def cancel(self): 236 pass 237 238 def publish(self): 239 pass 240 241 class Todo(Handler): 242 243 "A to-do item handler." 244 245 def add(self): 246 pass 247 248 def cancel(self): 249 pass 250 251 def counter(self): 252 253 "Since this handler does not send requests, it will not handle replies." 254 255 pass 256 257 def declinecounter(self): 258 259 """ 260 Since this handler does not send counter proposals, it will not handle 261 replies to such proposals. 262 """ 263 264 pass 265 266 def publish(self): 267 pass 268 269 def refresh(self): 270 pass 271 272 def reply(self): 273 274 "Since this handler does not send requests, it will not handle replies." 275 276 pass 277 278 def request(self): 279 pass 280 281 # Handler registry. 282 283 handlers = [ 284 ("VFREEBUSY", Freebusy), 285 ("VEVENT", Event), 286 ("VTODO", Todo), 287 ("VJOURNAL", Journal), 288 ] 289 290 # vim: tabstop=4 expandtab shiftwidth=4