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 # Update the fallback time zone information in the object. 54 55 self.obj.set_tzid(self.get_tzid()) 56 57 def _add(self): 58 59 "Add a recurrence for the current object." 60 61 if not Client.is_participating(self): 62 return False 63 64 # Check for event using UID. 65 66 if not self.have_new_object(): 67 return False 68 69 # Ignore unknown objects. 70 71 if not self.get_stored_object_version(): 72 return 73 74 # Record the event as a recurrence of the parent object. 75 76 self.update_recurrenceid() 77 78 # Set the additional occurrence. 79 80 self.store.set_event(self.user, self.uid, self.recurrenceid, self.obj.to_node()) 81 82 # Remove any previous cancellations involving this event. 83 84 self.store.remove_cancellation(self.user, self.uid, self.recurrenceid) 85 86 # Update free/busy information. 87 88 self.update_event_in_freebusy() 89 90 return True 91 92 def _record(self, from_organiser=True, counter=False): 93 94 """ 95 Record details from the current object given a message originating 96 from an organiser if 'from_organiser' is set to a true value. 97 """ 98 99 if not Client.is_participating(self): 100 return False 101 102 # Check for a new event, tolerating not-strictly-new events if the 103 # attendee is responding. 104 105 if not self.have_new_object(strict=from_organiser): 106 return False 107 108 # Update the object. 109 110 if from_organiser: 111 112 # Set the complete event or an additional occurrence. 113 114 self.store.set_event(self.user, self.uid, self.recurrenceid, self.obj.to_node()) 115 116 # Remove additional recurrences if handling a complete event. 117 # Also remove any previous cancellations involving this event. 118 119 if not self.recurrenceid: 120 self.store.remove_recurrences(self.user, self.uid) 121 self.store.remove_cancellations(self.user, self.uid) 122 else: 123 self.store.remove_cancellation(self.user, self.uid, self.recurrenceid) 124 125 else: 126 # Occurrences that are still part of a parent object are separated, 127 # attendance information transferred, and the free/busy details 128 # updated. 129 130 if self.is_newly_separated_occurrence(): 131 self.make_separate_occurrence(for_organiser=not from_organiser) 132 133 # Obtain valid attendees, merging their attendance with the stored 134 # object. 135 136 else: 137 attendees = self.require_attendees(from_organiser) 138 self.merge_attendance(attendees) 139 140 # Remove any associated request. 141 142 if from_organiser or self.has_indicated_attendance(): 143 self.store.dequeue_request(self.user, self.uid, self.recurrenceid) 144 self.store.remove_counters(self.user, self.uid, self.recurrenceid) 145 146 # Update free/busy information. 147 148 if not counter: 149 self.update_event_in_freebusy(from_organiser) 150 151 # For countered proposals, record the offer in the resource's 152 # free/busy collection. 153 154 else: 155 self.update_event_in_freebusy_offers() 156 157 return True 158 159 def _remove(self): 160 161 """ 162 Remove details from the current object given a message originating 163 from an organiser if 'from_organiser' is set to a true value. 164 """ 165 166 if not Client.is_participating(self): 167 return False 168 169 # Check for event using UID. 170 171 if not self.have_new_object(): 172 return False 173 174 # Obtain any stored object, using parent object details if a newly- 175 # indicated occurrence is referenced. 176 177 obj = self.get_stored_object_version() 178 old = not obj and self.get_parent_object() or obj 179 180 if not old: 181 return False 182 183 # Only cancel the event completely if all attendees are given. 184 185 attendees = old.get_uri_map("ATTENDEE") 186 all_attendees = set(attendees.keys()) 187 given_attendees = set(self.obj.get_uri_values("ATTENDEE")) 188 cancel_entire_event = not all_attendees.difference(given_attendees) 189 190 # Update the recipient's record of the organiser's schedule. 191 192 self.remove_freebusy_from_organiser(self.obj.get_value("ORGANIZER")) 193 194 # Otherwise, remove the given attendees and update the event. 195 196 if not cancel_entire_event and obj: 197 for attendee in given_attendees: 198 if attendees.has_key(attendee): 199 del attendees[attendee] 200 obj["ATTENDEE"] = attendees.items() 201 202 # Update the stored object with sequence information. 203 204 if obj: 205 obj["SEQUENCE"] = self.obj.get_items("SEQUENCE") or [] 206 obj["DTSTAMP"] = self.obj.get_items("DTSTAMP") or [] 207 208 # Update free/busy information. 209 210 if cancel_entire_event or self.user in given_attendees: 211 self.remove_event_from_freebusy() 212 self.remove_freebusy_from_attendees(attendees) 213 214 # Set the complete event if not an additional occurrence. For any newly- 215 # indicated occurrence, use the received event details. 216 217 self.store.set_event(self.user, self.uid, self.recurrenceid, (obj or self.obj).to_node()) 218 219 # Perform any cancellation after recording the latest state of the 220 # event. 221 222 if cancel_entire_event: 223 self.store.cancel_event(self.user, self.uid, self.recurrenceid) 224 225 # Remove any associated request. 226 227 self.store.dequeue_request(self.user, self.uid, self.recurrenceid) 228 self.store.remove_counters(self.user, self.uid, self.recurrenceid) 229 230 return True 231 232 def _declinecounter(self): 233 234 "Remove any counter-proposals for the given event." 235 236 if not Client.is_participating(self): 237 return False 238 239 # Check for event using UID. 240 241 if not self.have_new_object(): 242 return False 243 244 self.remove_counters(self.obj.get_uri_values("ATTENDEE")) 245 246 class Event(PersonHandler): 247 248 "An event handler." 249 250 def add(self): 251 252 "Record the addition of a recurrence to an event." 253 254 self._add() 255 256 def cancel(self): 257 258 "Remove an event or a recurrence." 259 260 self._remove() 261 262 def counter(self): 263 264 "Record an offer made by a counter-proposal." 265 266 self._record(False, True) 267 268 def declinecounter(self): 269 270 "Expire any offer made by a counter-proposal." 271 272 self._declinecounter() 273 274 def publish(self): 275 276 "Published events are recorded." 277 278 self._record(True) 279 280 def refresh(self): 281 282 "Requests to refresh events do not provide event information." 283 284 pass 285 286 def reply(self): 287 288 "Replies to requests are inspected for attendee information." 289 290 self._record(False) 291 292 def request(self): 293 294 "Record events sent for potential scheduling." 295 296 self._record(True) 297 298 # Handler registry. 299 300 handlers = [ 301 ("VEVENT", Event), 302 ] 303 304 # vim: tabstop=4 expandtab shiftwidth=4