1 #!/usr/bin/env python 2 3 """ 4 Handlers for a person for whom scheduling is performed, inspecting outgoing 5 messages to obtain scheduling done externally. 6 7 Copyright (C) 2014, 2015, 2016, 2017 Paul Boddie <paul@boddie.org.uk> 8 9 This program is free software; you can redistribute it and/or modify it under 10 the terms of the GNU General Public License as published by the Free Software 11 Foundation; either version 3 of the License, or (at your option) any later 12 version. 13 14 This program is distributed in the hope that it will be useful, but WITHOUT 15 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 16 FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 17 details. 18 19 You should have received a copy of the GNU General Public License along with 20 this program. If not, see <http://www.gnu.org/licenses/>. 21 """ 22 23 from imiptools.client import Client 24 from imiptools.handlers import Handler 25 from imiptools.handlers.common import CommonEvent 26 27 class PersonHandler(CommonEvent, Handler): 28 29 "Handling mechanisms specific to people." 30 31 def set_identity(self, method): 32 33 """ 34 Set the current user for the current object in the context of the given 35 'method'. It is usually set when initialising the handler, using the 36 recipient details, but outgoing messages do not reference the recipient 37 in this way. 38 """ 39 40 if self.obj and not self.user: 41 from_organiser = method in self.organiser_methods 42 if from_organiser: 43 self.user = self.obj.get_uri("ORGANIZER") 44 45 # Since there may be many attendees in an attendee-provided outgoing 46 # message, because counter-proposals can have more than one 47 # attendee, the attendee originating from the calendar system is 48 # chosen. 49 50 else: 51 self.user = self.get_sending_attendee() 52 53 def _add(self): 54 55 "Add a recurrence for the current object." 56 57 if not Client.is_participating(self): 58 return False 59 60 # Check for event using UID. 61 62 if not self.have_new_object(): 63 return False 64 65 # Ignore unknown objects. 66 67 if not self.get_stored_object_version(): 68 return 69 70 # Record the event as a recurrence of the parent object. 71 72 self.update_recurrenceid() 73 74 # Set the additional occurrence. 75 76 self.store.set_event(self.user, self.uid, self.recurrenceid, self.obj.to_node()) 77 78 # Remove any previous cancellations involving this event. 79 80 self.store.remove_cancellation(self.user, self.uid, self.recurrenceid) 81 82 # Update free/busy information. 83 84 self.update_event_in_freebusy() 85 86 return True 87 88 def _record(self, from_organiser=True, counter=False): 89 90 """ 91 Record details from the current object given a message originating 92 from an organiser if 'from_organiser' is set to a true value. 93 """ 94 95 if not Client.is_participating(self): 96 return False 97 98 # Check for a new event, tolerating not-strictly-new events if the 99 # attendee is responding. 100 101 if not self.have_new_object(strict=from_organiser): 102 return False 103 104 # Update the object. 105 106 if from_organiser: 107 108 # Set the complete event or an additional occurrence. 109 110 self.store.set_event(self.user, self.uid, self.recurrenceid, self.obj.to_node()) 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 else: 122 # Occurrences that are still part of a parent object are separated, 123 # attendance information transferred, and the free/busy details 124 # updated. 125 126 if self.is_newly_separated_occurrence(): 127 self.make_separate_occurrence(for_organiser=not from_organiser) 128 129 # Obtain valid attendees, merging their attendance with the stored 130 # object. 131 132 else: 133 attendees = self.require_attendees(from_organiser) 134 self.merge_attendance(attendees) 135 136 # Remove any associated request. 137 138 if from_organiser or self.has_indicated_attendance(): 139 self.store.dequeue_request(self.user, self.uid, self.recurrenceid) 140 self.store.remove_counters(self.user, self.uid, self.recurrenceid) 141 142 # Update free/busy information. 143 144 if not counter: 145 self.update_event_in_freebusy(from_organiser) 146 147 # For countered proposals, record the offer in the resource's 148 # free/busy collection. 149 150 else: 151 self.update_event_in_freebusy_offers() 152 153 return True 154 155 def _remove(self): 156 157 """ 158 Remove details from the current object given a message originating 159 from an organiser if 'from_organiser' is set to a true value. 160 """ 161 162 if not Client.is_participating(self): 163 return False 164 165 # Check for event using UID. 166 167 if not self.have_new_object(): 168 return False 169 170 # Obtain any stored object, using parent object details if a newly- 171 # indicated occurrence is referenced. 172 173 obj = self.get_stored_object_version() 174 old = not obj and self.get_parent_object() or obj 175 176 if not old: 177 return False 178 179 # Only cancel the event completely if all attendees are given. 180 181 attendees = old.get_uri_map("ATTENDEE") 182 all_attendees = set(attendees.keys()) 183 given_attendees = set(self.obj.get_uri_values("ATTENDEE")) 184 cancel_entire_event = not all_attendees.difference(given_attendees) 185 186 # Update the recipient's record of the organiser's schedule. 187 188 self.remove_freebusy_from_organiser(self.obj.get_value("ORGANIZER")) 189 190 # Otherwise, remove the given attendees and update the event. 191 192 if not cancel_entire_event and obj: 193 for attendee in given_attendees: 194 if attendees.has_key(attendee): 195 del attendees[attendee] 196 obj["ATTENDEE"] = attendees.items() 197 198 # Update the stored object with sequence information. 199 200 if obj: 201 obj["SEQUENCE"] = self.obj.get_items("SEQUENCE") or [] 202 obj["DTSTAMP"] = self.obj.get_items("DTSTAMP") or [] 203 204 # Update free/busy information. 205 206 if cancel_entire_event or self.user in given_attendees: 207 self.remove_event_from_freebusy() 208 self.remove_freebusy_from_attendees(attendees) 209 210 # Set the complete event if not an additional occurrence. For any newly- 211 # indicated occurrence, use the received event details. 212 213 self.store.set_event(self.user, self.uid, self.recurrenceid, (obj or self.obj).to_node()) 214 215 # Perform any cancellation after recording the latest state of the 216 # event. 217 218 if cancel_entire_event: 219 self.store.cancel_event(self.user, self.uid, self.recurrenceid) 220 221 # Remove any associated request. 222 223 self.store.dequeue_request(self.user, self.uid, self.recurrenceid) 224 self.store.remove_counters(self.user, self.uid, self.recurrenceid) 225 226 return True 227 228 def _declinecounter(self): 229 230 "Remove any counter-proposals for the given event." 231 232 if not Client.is_participating(self): 233 return False 234 235 # Check for event using UID. 236 237 if not self.have_new_object(): 238 return False 239 240 self.remove_counters(self.obj.get_uri_values("ATTENDEE")) 241 242 class Event(PersonHandler): 243 244 "An event handler." 245 246 def add(self): 247 248 "Record the addition of a recurrence to an event." 249 250 self._add() 251 252 def cancel(self): 253 254 "Remove an event or a recurrence." 255 256 self._remove() 257 258 def counter(self): 259 260 "Record an offer made by a counter-proposal." 261 262 self._record(False, True) 263 264 def declinecounter(self): 265 266 "Expire any offer made by a counter-proposal." 267 268 self._declinecounter() 269 270 def publish(self): 271 272 "Published events are recorded." 273 274 self._record(True) 275 276 def refresh(self): 277 278 "Requests to refresh events do not provide event information." 279 280 pass 281 282 def reply(self): 283 284 "Replies to requests are inspected for attendee information." 285 286 self._record(False) 287 288 def request(self): 289 290 "Record events sent for potential scheduling." 291 292 self._record(True) 293 294 # Handler registry. 295 296 handlers = [ 297 ("VEVENT", Event), 298 ] 299 300 # vim: tabstop=4 expandtab shiftwidth=4