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