1 #!/usr/bin/env python 2 3 """ 4 Handlers for a resource. 5 6 Copyright (C) 2014, 2015, 2016 Paul Boddie <paul@boddie.org.uk> 7 8 This program is free software; you can redistribute it and/or modify it under 9 the terms of the GNU General Public License as published by the Free Software 10 Foundation; either version 3 of the License, or (at your option) any later 11 version. 12 13 This program is distributed in the hope that it will be useful, but WITHOUT 14 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 15 FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 16 details. 17 18 You should have received a copy of the GNU General Public License along with 19 this program. If not, see <http://www.gnu.org/licenses/>. 20 """ 21 22 from email.mime.text import MIMEText 23 from imiptools.data import get_address, uri_dict 24 from imiptools.handlers import Handler 25 from imiptools.handlers.common import CommonFreebusy, CommonEvent 26 from imiptools.handlers.scheduling import apply_scheduling_functions, \ 27 confirm_scheduling, \ 28 finish_scheduling, \ 29 retract_scheduling 30 31 class ResourceHandler(CommonEvent, Handler): 32 33 "Handling mechanisms specific to resources." 34 35 def _process(self, handle_for_attendee): 36 37 """ 38 Record details from the incoming message, using the given 39 'handle_for_attendee' callable to process any valid message 40 appropriately. 41 """ 42 43 oa = self.require_organiser_and_attendees() 44 if not oa: 45 return None 46 47 organiser_item, attendees = oa 48 49 # Process for the current user, a resource as attendee. 50 51 if not self.have_new_object(): 52 return None 53 54 # Collect response objects produced when handling the request. 55 56 handle_for_attendee() 57 58 def _add_for_attendee(self): 59 60 """ 61 Attempt to add a recurrence to an existing object for the current user. 62 This does not request a response concerning participation, apparently. 63 """ 64 65 # Request details where configured, doing so for unknown objects anyway. 66 67 if self.will_refresh(): 68 self.make_refresh() 69 return 70 71 # Record the event as a recurrence of the parent object. 72 73 self.update_recurrenceid() 74 self.store.set_event(self.user, self.uid, self.recurrenceid, self.obj.to_node()) 75 76 # Remove any previous cancellations involving this event. 77 78 self.store.remove_cancellation(self.user, self.uid, self.recurrenceid) 79 80 # Update free/busy information. 81 82 self.update_event_in_freebusy(for_organiser=False) 83 84 # Confirm the scheduling of the recurrence. 85 86 self.confirm_scheduling() 87 88 def _schedule_for_attendee(self): 89 90 "Attempt to schedule the current object for the current user." 91 92 attendee_attr = uri_dict(self.obj.get_value_map("ATTENDEE"))[self.user] 93 delegate = None 94 95 # Attempt to schedule the event. 96 97 scheduled, description = self.schedule() 98 99 try: 100 # Update the participation of the resource in the object. 101 # Update free/busy information. 102 103 if scheduled in ("ACCEPTED", "DECLINED"): 104 method = "REPLY" 105 attendee_attr = self.update_participation(scheduled) 106 107 self.update_event_in_freebusy(for_organiser=False) 108 self.remove_event_from_freebusy_offers() 109 110 # Set the complete event or an additional occurrence. 111 112 event = self.obj.to_node() 113 self.store.set_event(self.user, self.uid, self.recurrenceid, event) 114 115 # Remove additional recurrences if handling a complete event. 116 # Also remove any previous cancellations involving this event. 117 118 if not self.recurrenceid: 119 self.store.remove_recurrences(self.user, self.uid) 120 self.store.remove_cancellations(self.user, self.uid) 121 else: 122 self.store.remove_cancellation(self.user, self.uid, self.recurrenceid) 123 124 if scheduled == "ACCEPTED": 125 self.confirm_scheduling() 126 127 # For delegated proposals, prepare a request to the delegate in 128 # addition to the usual response. 129 130 elif scheduled == "DELEGATED": 131 method = "REPLY" 132 attendee_attr = self.update_participation("DELEGATED") 133 134 # The recipient will have indicated the delegate whose details 135 # will have been added to the object. 136 137 delegated_to = attendee_attr["DELEGATED-TO"] 138 delegate = delegated_to and delegated_to[0] 139 140 # For countered proposals, record the offer in the resource's 141 # free/busy collection. 142 143 elif scheduled == "COUNTER": 144 method = "COUNTER" 145 self.update_event_in_freebusy_offers() 146 147 # For inappropriate periods, reply declining participation. 148 149 else: 150 method = "REPLY" 151 attendee_attr = self.update_participation("DECLINED") 152 153 # Confirm any scheduling. 154 155 finally: 156 self.finish_scheduling() 157 158 # Determine the recipients of the outgoing messages. 159 160 recipients = map(get_address, self.obj.get_values("ORGANIZER")) 161 162 # Add any description of the scheduling decision. 163 164 self.add_result(None, recipients, MIMEText(description)) 165 166 # Make a version of the object with just this attendee, update the 167 # DTSTAMP in the response, and return the object for sending. 168 169 self.update_sender(attendee_attr) 170 attendees = [(self.user, attendee_attr)] 171 172 # Add the delegate if delegating (RFC 5546 being inconsistent here since 173 # it provides an example reply to the organiser without the delegate). 174 175 if delegate: 176 delegate_attr = uri_dict(self.obj.get_value_map("ATTENDEE"))[delegate] 177 attendees.append((delegate, delegate_attr)) 178 179 # Reply to the delegator in addition to the organiser if replying to a 180 # delegation request. 181 182 delegator = self.is_delegation() 183 if delegator: 184 delegator_attr = uri_dict(self.obj.get_value_map("ATTENDEE"))[delegator] 185 attendees.append((delegator, delegator_attr)) 186 recipients.append(get_address(delegator)) 187 188 # Prepare the response for the organiser plus any delegator. 189 190 self.obj["ATTENDEE"] = attendees 191 self.update_dtstamp() 192 self.add_result(method, recipients, self.object_to_part(method, self.obj)) 193 194 # If delegating, send a request to the delegate. 195 196 if delegate: 197 method = "REQUEST" 198 self.add_result(method, [get_address(delegate)], self.object_to_part(method, self.obj)) 199 200 def _cancel_for_attendee(self): 201 202 """ 203 Cancel for the current user their attendance of the event described by 204 the current object. 205 """ 206 207 # Update free/busy information. 208 209 self.remove_event_from_freebusy() 210 211 # Update the stored event and cancel it. 212 213 self.store.set_event(self.user, self.uid, self.recurrenceid, self.obj.to_node()) 214 self.store.cancel_event(self.user, self.uid, self.recurrenceid) 215 216 # Retract the scheduling of the event. 217 218 self.retract_scheduling() 219 220 def _revoke_for_attendee(self): 221 222 "Revoke any counter-proposal recorded as a free/busy offer." 223 224 self.remove_event_from_freebusy_offers() 225 226 # Scheduling details. 227 228 def get_scheduling_functions(self): 229 230 "Return the scheduling functions for the resource." 231 232 return self.get_preferences().get("scheduling_function", 233 "schedule_in_freebusy").split("\n") 234 235 def schedule(self): 236 237 """ 238 Attempt to schedule the current object, returning an indication of the 239 kind of response to be returned: "COUNTER" for counter-proposals, 240 "ACCEPTED" for acceptances, "DECLINED" for rejections, and None for 241 invalid requests. 242 """ 243 244 return apply_scheduling_functions(self) 245 246 def confirm_scheduling(self): 247 248 "Confirm that this event has been scheduled." 249 250 confirm_scheduling(self) 251 252 def finish_scheduling(self): 253 254 "Finish the scheduling, unlocking resources where appropriate." 255 256 finish_scheduling(self) 257 258 def retract_scheduling(self): 259 260 "Retract this event from scheduling records." 261 262 retract_scheduling(self) 263 264 class Event(ResourceHandler): 265 266 "An event handler." 267 268 def add(self): 269 270 "Add a new occurrence to an existing event." 271 272 self._process(self._add_for_attendee) 273 274 def cancel(self): 275 276 "Cancel attendance for attendees." 277 278 self._process(self._cancel_for_attendee) 279 280 def counter(self): 281 282 "Since this handler does not send requests, it will not handle replies." 283 284 pass 285 286 def declinecounter(self): 287 288 "Revoke any counter-proposal." 289 290 self._process(self._revoke_for_attendee) 291 292 def publish(self): 293 294 """ 295 Resources only consider events sent as requests, not generally published 296 events. 297 """ 298 299 pass 300 301 def refresh(self): 302 303 """ 304 Refresh messages are typically sent to event organisers, but resources 305 do not act as organisers themselves. 306 """ 307 308 pass 309 310 def reply(self): 311 312 "Since this handler does not send requests, it will not handle replies." 313 314 pass 315 316 def request(self): 317 318 """ 319 Respond to a request by preparing a reply containing accept/decline 320 information for the recipient. 321 322 No support for countering requests is implemented. 323 """ 324 325 self._process(self._schedule_for_attendee) 326 327 class Freebusy(CommonFreebusy, Handler): 328 329 "A free/busy handler." 330 331 def publish(self): 332 333 "Resources ignore generally published free/busy information." 334 335 self._record_freebusy(from_organiser=True) 336 337 def reply(self): 338 339 "Since this handler does not send requests, it will not handle replies." 340 341 pass 342 343 # request provided by CommonFreeBusy.request 344 345 # Handler registry. 346 347 handlers = [ 348 ("VFREEBUSY", Freebusy), 349 ("VEVENT", Event), 350 ] 351 352 # vim: tabstop=4 expandtab shiftwidth=4