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 123 # Update free/busy information. 124 125 if not counter: 126 self.update_event_in_freebusy(from_organiser) 127 128 # For countered proposals, record the offer in the resource's 129 # free/busy collection. 130 131 else: 132 self.update_event_in_freebusy_offers() 133 134 return True 135 136 def _remove(self): 137 138 """ 139 Remove details from the current object given a message originating 140 from an organiser if 'from_organiser' is set to a true value. 141 """ 142 143 if not Client.is_participating(self): 144 return False 145 146 # Check for event using UID. 147 148 if not self.have_new_object(): 149 return False 150 151 # Obtain any stored object, using parent object details if a newly- 152 # indicated occurrence is referenced. 153 154 obj = self.get_stored_object_version() 155 old = not obj and self.get_parent_object() or obj 156 157 if not old: 158 return False 159 160 # Only cancel the event completely if all attendees are given. 161 162 attendees = uri_dict(old.get_value_map("ATTENDEE")) 163 all_attendees = set(attendees.keys()) 164 given_attendees = set(uri_values(self.obj.get_values("ATTENDEE"))) 165 cancel_entire_event = not all_attendees.difference(given_attendees) 166 167 # Update the recipient's record of the organiser's schedule. 168 169 self.remove_freebusy_from_organiser(self.obj.get_value("ORGANIZER")) 170 171 # Otherwise, remove the given attendees and update the event. 172 173 if not cancel_entire_event and obj: 174 for attendee in given_attendees: 175 if attendees.has_key(attendee): 176 del attendees[attendee] 177 obj["ATTENDEE"] = attendees.items() 178 179 # Update the stored object with sequence information. 180 181 if obj: 182 obj["SEQUENCE"] = self.obj.get_items("SEQUENCE") or [] 183 obj["DTSTAMP"] = self.obj.get_items("DTSTAMP") or [] 184 185 # Update free/busy information. 186 187 if cancel_entire_event or self.user in given_attendees: 188 self.remove_event_from_freebusy() 189 190 # Set the complete event if not an additional occurrence. For any newly- 191 # indicated occurrence, use the received event details. 192 193 self.store.set_event(self.user, self.uid, self.recurrenceid, (obj or self.obj).to_node()) 194 195 # Perform any cancellation after recording the latest state of the 196 # event. 197 198 if cancel_entire_event: 199 self.store.cancel_event(self.user, self.uid, self.recurrenceid) 200 self.store.dequeue_request(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 206 return True 207 208 def _declinecounter(self): 209 210 "Remove any counter-proposals for the given event." 211 212 if not Client.is_participating(self): 213 return False 214 215 # Check for event using UID. 216 217 if not self.have_new_object(): 218 return False 219 220 self.store.dequeue_request(self.user, self.uid, self.recurrenceid) 221 222 class Event(PersonHandler): 223 224 "An event handler." 225 226 def add(self): 227 228 "Record the addition of a recurrence to an event." 229 230 self._add() 231 232 def cancel(self): 233 234 "Remove an event or a recurrence." 235 236 self._remove() 237 238 def counter(self): 239 240 "Record an offer made by a counter-proposal." 241 242 self._record(False, True) 243 244 def declinecounter(self): 245 246 "Expire any offer made by a counter-proposal." 247 248 self._declinecounter() 249 250 def publish(self): 251 252 "Published events are recorded." 253 254 self._record(True) 255 256 def refresh(self): 257 258 "Requests to refresh events do not provide event information." 259 260 pass 261 262 def reply(self): 263 264 "Replies to requests are inspected for attendee information." 265 266 self._record(False) 267 268 def request(self): 269 270 "Record events sent for potential scheduling." 271 272 self._record(True) 273 274 # Handler registry. 275 276 handlers = [ 277 ("VEVENT", Event), 278 ] 279 280 # vim: tabstop=4 expandtab shiftwidth=4