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