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