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 imiptools.data import get_address, to_part, uri_dict 23 from imiptools.handlers import Handler 24 from imiptools.handlers.common import CommonFreebusy, CommonEvent 25 from imiptools.handlers.scheduling import apply_scheduling_functions, \ 26 confirm_scheduling, retract_scheduling 27 28 class ResourceHandler(CommonEvent, Handler): 29 30 "Handling mechanisms specific to resources." 31 32 def _process(self, handle_for_attendee): 33 34 """ 35 Record details from the incoming message, using the given 36 'handle_for_attendee' callable to process any valid message 37 appropriately. 38 """ 39 40 oa = self.require_organiser_and_attendees() 41 if not oa: 42 return None 43 44 organiser_item, attendees = oa 45 46 # Process for the current user, a resource as attendee. 47 48 if not self.have_new_object(): 49 return None 50 51 # Collect response objects produced when handling the request. 52 53 handle_for_attendee() 54 55 def _add_for_attendee(self): 56 57 """ 58 Attempt to add a recurrence to an existing object for the current user. 59 This does not request a response concerning participation, apparently. 60 """ 61 62 # Request details where configured, doing so for unknown objects anyway. 63 64 if self.will_refresh(): 65 self.make_refresh() 66 return 67 68 # Record the event as a recurrence of the parent object. 69 70 self.update_recurrenceid() 71 self.store.set_event(self.user, self.uid, self.recurrenceid, self.obj.to_node()) 72 73 # Remove any previous cancellations involving this event. 74 75 self.store.remove_cancellation(self.user, self.uid, self.recurrenceid) 76 77 # Update free/busy information. 78 79 self.update_event_in_freebusy(for_organiser=False) 80 81 # Confirm the scheduling of the recurrence. 82 83 self.confirm_scheduling() 84 85 def _schedule_for_attendee(self): 86 87 "Attempt to schedule the current object for the current user." 88 89 attendee_attr = uri_dict(self.obj.get_value_map("ATTENDEE"))[self.user] 90 scheduled = self.schedule() 91 92 # Update the participation of the resource in the object. 93 # Update free/busy information. 94 95 if scheduled in ("ACCEPTED", "DECLINED"): 96 method = "REPLY" 97 attendee_attr = self.update_participation(scheduled) 98 99 self.update_event_in_freebusy(for_organiser=False) 100 self.remove_event_from_freebusy_offers() 101 102 # Set the complete event or an additional occurrence. 103 104 event = self.obj.to_node() 105 self.store.set_event(self.user, self.uid, self.recurrenceid, event) 106 107 # Remove additional recurrences if handling a complete event. 108 # Also remove any previous cancellations involving this event. 109 110 if not self.recurrenceid: 111 self.store.remove_recurrences(self.user, self.uid) 112 self.store.remove_cancellations(self.user, self.uid) 113 else: 114 self.store.remove_cancellation(self.user, self.uid, self.recurrenceid) 115 116 # Confirm any scheduling. 117 118 if scheduled == "ACCEPTED": 119 self.confirm_scheduling() 120 121 # For countered proposals, record the offer in the resource's 122 # free/busy collection. 123 124 elif scheduled == "COUNTER": 125 method = "COUNTER" 126 self.update_event_in_freebusy_offers() 127 128 # For inappropriate periods, reply declining participation. 129 130 else: 131 method = "REPLY" 132 attendee_attr = self.update_participation("DECLINED") 133 134 # Make a version of the object with just this attendee, update the 135 # DTSTAMP in the response, and return the object for sending. 136 137 self.update_sender(attendee_attr) 138 self.obj["ATTENDEE"] = [(self.user, attendee_attr)] 139 self.update_dtstamp() 140 self.add_result(method, map(get_address, self.obj.get_values("ORGANIZER")), to_part(method, [self.obj.to_node()])) 141 142 def _cancel_for_attendee(self): 143 144 """ 145 Cancel for the current user their attendance of the event described by 146 the current object. 147 """ 148 149 # Update free/busy information. 150 151 self.remove_event_from_freebusy() 152 153 # Update the stored event and cancel it. 154 155 self.store.set_event(self.user, self.uid, self.recurrenceid, self.obj.to_node()) 156 self.store.cancel_event(self.user, self.uid, self.recurrenceid) 157 158 # Retract the scheduling of the event. 159 160 self.retract_scheduling() 161 162 def _revoke_for_attendee(self): 163 164 "Revoke any counter-proposal recorded as a free/busy offer." 165 166 self.remove_event_from_freebusy_offers() 167 168 # Scheduling details. 169 170 def schedule(self): 171 172 """ 173 Attempt to schedule the current object, returning an indication of the 174 kind of response to be returned: "COUNTER" for counter-proposals, 175 "ACCEPTED" for acceptances, "DECLINED" for rejections, and None for 176 invalid requests. 177 """ 178 179 # Obtain a list of scheduling functions. 180 181 functions = self.get_preferences().get("scheduling_function", 182 "schedule_in_freebusy").split("\n") 183 184 return apply_scheduling_functions(functions, self) 185 186 def confirm_scheduling(self): 187 188 "Confirm that this event has been scheduled." 189 190 functions = self.get_preferences().get("confirmation_function") 191 192 if functions: 193 confirm_scheduling(functions.split("\n"), self) 194 195 def retract_scheduling(self): 196 197 "Retract this event from scheduling records." 198 199 functions = self.get_preferences().get("retraction_function") 200 201 if functions: 202 retract_scheduling(functions.split("\n"), self) 203 204 class Event(ResourceHandler): 205 206 "An event handler." 207 208 def add(self): 209 210 "Add a new occurrence to an existing event." 211 212 self._process(self._add_for_attendee) 213 214 def cancel(self): 215 216 "Cancel attendance for attendees." 217 218 self._process(self._cancel_for_attendee) 219 220 def counter(self): 221 222 "Since this handler does not send requests, it will not handle replies." 223 224 pass 225 226 def declinecounter(self): 227 228 "Revoke any counter-proposal." 229 230 self._process(self._revoke_for_attendee) 231 232 def publish(self): 233 234 """ 235 Resources only consider events sent as requests, not generally published 236 events. 237 """ 238 239 pass 240 241 def refresh(self): 242 243 """ 244 Refresh messages are typically sent to event organisers, but resources 245 do not act as organisers themselves. 246 """ 247 248 pass 249 250 def reply(self): 251 252 "Since this handler does not send requests, it will not handle replies." 253 254 pass 255 256 def request(self): 257 258 """ 259 Respond to a request by preparing a reply containing accept/decline 260 information for the recipient. 261 262 No support for countering requests is implemented. 263 """ 264 265 self._process(self._schedule_for_attendee) 266 267 class Freebusy(CommonFreebusy, Handler): 268 269 "A free/busy handler." 270 271 def publish(self): 272 273 "Resources ignore generally published free/busy information." 274 275 self._record_freebusy(from_organiser=True) 276 277 def reply(self): 278 279 "Since this handler does not send requests, it will not handle replies." 280 281 pass 282 283 # request provided by CommonFreeBusy.request 284 285 # Handler registry. 286 287 handlers = [ 288 ("VFREEBUSY", Freebusy), 289 ("VEVENT", Event), 290 ] 291 292 # vim: tabstop=4 expandtab shiftwidth=4