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