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