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 delegates = None 94 95 # Attempt to schedule the event. 96 97 try: 98 scheduled, description = self.schedule() 99 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 delegates 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 delegates = attendee_attr["DELEGATED-TO"] 138 139 # For countered proposals, record the offer in the resource's 140 # free/busy collection. 141 142 elif scheduled == "COUNTER": 143 method = "COUNTER" 144 self.update_event_in_freebusy_offers() 145 146 # For inappropriate periods, reply declining participation. 147 148 else: 149 method = "REPLY" 150 attendee_attr = self.update_participation("DECLINED") 151 152 # Confirm any scheduling. 153 154 finally: 155 self.finish_scheduling() 156 157 # Determine the recipients of the outgoing messages. 158 159 recipients = map(get_address, self.obj.get_values("ORGANIZER")) 160 161 # Add any description of the scheduling decision. 162 163 self.add_result(None, recipients, MIMEText(description)) 164 165 # Make a version of the object with just this attendee, update the 166 # DTSTAMP in the response, and return the object for sending. 167 168 self.update_sender(attendee_attr) 169 attendees = [(self.user, attendee_attr)] 170 171 # Add delegates if delegating (RFC 5546 being inconsistent here since 172 # it provides an example reply to the organiser without the delegate). 173 174 if delegates: 175 for delegate in delegates: 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 delegators = self.is_delegation() 183 if delegators: 184 for delegator in delegators: 185 delegator_attr = uri_dict(self.obj.get_value_map("ATTENDEE"))[delegator] 186 attendees.append((delegator, delegator_attr)) 187 recipients.append(get_address(delegator)) 188 189 # Prepare the response for the organiser plus any delegator. 190 191 self.obj["ATTENDEE"] = attendees 192 self.update_dtstamp() 193 self.add_result(method, recipients, self.object_to_part(method, self.obj)) 194 195 # If delegating, send a request to the delegates. 196 197 if delegates: 198 method = "REQUEST" 199 self.add_result(method, map(get_address, delegates), self.object_to_part(method, self.obj)) 200 201 def _cancel_for_attendee(self): 202 203 """ 204 Cancel for the current user their attendance of the event described by 205 the current object. 206 """ 207 208 # Update free/busy information. 209 210 self.remove_event_from_freebusy() 211 212 # Update the stored event and cancel it. 213 214 self.store.set_event(self.user, self.uid, self.recurrenceid, self.obj.to_node()) 215 self.store.cancel_event(self.user, self.uid, self.recurrenceid) 216 217 # Retract the scheduling of the event. 218 219 self.retract_scheduling() 220 221 def _revoke_for_attendee(self): 222 223 "Revoke any counter-proposal recorded as a free/busy offer." 224 225 self.remove_event_from_freebusy_offers() 226 227 # Scheduling details. 228 229 def get_scheduling_functions(self): 230 231 "Return the scheduling functions for the resource." 232 233 return self.get_preferences().get("scheduling_function", 234 "schedule_in_freebusy").split("\n") 235 236 def schedule(self): 237 238 """ 239 Attempt to schedule the current object, returning an indication of the 240 kind of response to be returned: "COUNTER" for counter-proposals, 241 "ACCEPTED" for acceptances, "DECLINED" for rejections, and None for 242 invalid requests. 243 """ 244 245 return apply_scheduling_functions(self) 246 247 def confirm_scheduling(self): 248 249 "Confirm that this event has been scheduled." 250 251 confirm_scheduling(self) 252 253 def finish_scheduling(self): 254 255 "Finish the scheduling, unlocking resources where appropriate." 256 257 finish_scheduling(self) 258 259 def retract_scheduling(self): 260 261 "Retract this event from scheduling records." 262 263 retract_scheduling(self) 264 265 class Event(ResourceHandler): 266 267 "An event handler." 268 269 def add(self): 270 271 "Add a new occurrence to an existing event." 272 273 self._process(self._add_for_attendee) 274 275 def cancel(self): 276 277 "Cancel attendance for attendees." 278 279 self._process(self._cancel_for_attendee) 280 281 def counter(self): 282 283 "Since this handler does not send requests, it will not handle replies." 284 285 pass 286 287 def declinecounter(self): 288 289 "Revoke any counter-proposal." 290 291 self._process(self._revoke_for_attendee) 292 293 def publish(self): 294 295 """ 296 Resources only consider events sent as requests, not generally published 297 events. 298 """ 299 300 pass 301 302 def refresh(self): 303 304 """ 305 Refresh messages are typically sent to event organisers, but resources 306 do not act as organisers themselves. 307 """ 308 309 pass 310 311 def reply(self): 312 313 "Since this handler does not send requests, it will not handle replies." 314 315 pass 316 317 def request(self): 318 319 """ 320 Respond to a request by preparing a reply containing accept/decline 321 information for the recipient. 322 323 No support for countering requests is implemented. 324 """ 325 326 self._process(self._schedule_for_attendee) 327 328 class Freebusy(CommonFreebusy, Handler): 329 330 "A free/busy handler." 331 332 def publish(self): 333 334 "Resources ignore generally published free/busy information." 335 336 self._record_freebusy(from_organiser=True) 337 338 def reply(self): 339 340 "Since this handler does not send requests, it will not handle replies." 341 342 pass 343 344 # request provided by CommonFreeBusy.request 345 346 # Handler registry. 347 348 handlers = [ 349 ("VFREEBUSY", Freebusy), 350 ("VEVENT", Event), 351 ] 352 353 # vim: tabstop=4 expandtab shiftwidth=4