imip-agent

imiptools/client.py

794:481df9da00f0
2015-09-29 Paul Boddie Added support for address usage when specifying attendees, along with usage of the CN attribute for attendees and organisers.
     1 #!/usr/bin/env python     2      3 """     4 Common calendar client utilities.     5      6 Copyright (C) 2014, 2015 Paul Boddie <paul@boddie.org.uk>     7      8 This program is free software; you can redistribute it and/or modify it under     9 the terms of the GNU General Public License as published by the Free Software    10 Foundation; either version 3 of the License, or (at your option) any later    11 version.    12     13 This program is distributed in the hope that it will be useful, but WITHOUT    14 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS    15 FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more    16 details.    17     18 You should have received a copy of the GNU General Public License along with    19 this program.  If not, see <http://www.gnu.org/licenses/>.    20 """    21     22 from datetime import datetime, timedelta    23 from imiptools import config    24 from imiptools.data import Object, get_address, get_uri, get_window_end, \    25                            is_new_object, make_freebusy, to_part, \    26                            uri_dict, uri_items, uri_values    27 from imiptools.dates import check_permitted_values, format_datetime, get_default_timezone, \    28                             get_duration, get_time, get_timestamp    29 from imiptools.period import can_schedule, remove_period, \    30                              remove_additional_periods, remove_affected_period, \    31                              update_freebusy    32 from imiptools.profile import Preferences    33 import imip_store    34     35 class Client:    36     37     "Common handler and manager methods."    38     39     default_window_size = 100    40     organiser_methods = "ADD", "CANCEL", "DECLINECOUNTER", "PUBLISH", "REQUEST"    41     42     def __init__(self, user, messenger=None, store=None, publisher=None, preferences_dir=None):    43     44         """    45         Initialise a calendar client with the current 'user', plus any    46         'messenger', 'store' and 'publisher' objects, indicating any specific    47         'preferences_dir'.    48         """    49     50         self.user = user    51         self.messenger = messenger    52         self.store = store or imip_store.FileStore()    53     54         try:    55             self.publisher = publisher or imip_store.FilePublisher()    56         except OSError:    57             self.publisher = None    58     59         self.preferences_dir = preferences_dir    60         self.preferences = None    61     62     # Store-related methods.    63     64     def acquire_lock(self):    65         self.store.acquire_lock(self.user)    66     67     def release_lock(self):    68         self.store.release_lock(self.user)    69     70     # Preferences-related methods.    71     72     def get_preferences(self):    73         if not self.preferences and self.user:    74             self.preferences = Preferences(self.user, self.preferences_dir)    75         return self.preferences    76     77     def get_user_attributes(self):    78         prefs = self.get_preferences()    79         return prefs and prefs.get_all(["CN"]) or {}    80     81     def get_tzid(self):    82         prefs = self.get_preferences()    83         return prefs and prefs.get("TZID") or get_default_timezone()    84     85     def get_window_size(self):    86         prefs = self.get_preferences()    87         try:    88             return prefs and int(prefs.get("window_size")) or self.default_window_size    89         except (TypeError, ValueError):    90             return self.default_window_size    91     92     def get_window_end(self):    93         return get_window_end(self.get_tzid(), self.get_window_size())    94     95     def is_participating(self):    96     97         "Return participation in the calendar system."    98     99         prefs = self.get_preferences()   100         return prefs and prefs.get("participating", config.PARTICIPATING_DEFAULT) != "no" or False   101    102     def is_sharing(self):   103    104         "Return whether free/busy information is being generally shared."   105    106         prefs = self.get_preferences()   107         return prefs and prefs.get("freebusy_sharing", config.SHARING_DEFAULT) == "share" or False   108    109     def is_bundling(self):   110    111         "Return whether free/busy information is being bundled in messages."   112    113         prefs = self.get_preferences()   114         return prefs and prefs.get("freebusy_bundling", config.BUNDLING_DEFAULT) == "always" or False   115    116     def is_notifying(self):   117    118         "Return whether recipients are notified about free/busy payloads."   119    120         prefs = self.get_preferences()   121         return prefs and prefs.get("freebusy_messages", config.NOTIFYING_DEFAULT) == "notify" or False   122    123     def is_publishing(self):   124    125         "Return whether free/busy information is being published as Web resources."   126    127         prefs = self.get_preferences()   128         return prefs and prefs.get("freebusy_publishing", config.PUBLISHING_DEFAULT) == "publish" or False   129    130     def is_refreshing(self):   131    132         "Return whether a recipient supports requests to refresh event details."   133    134         prefs = self.get_preferences()   135         return prefs and prefs.get("event_refreshing", config.REFRESHING_DEFAULT) == "always" or False   136    137     def allow_add(self):   138         return self.get_add_method_response() in ("add", "refresh")   139    140     def get_add_method_response(self):   141         prefs = self.get_preferences()   142         return prefs and prefs.get("add_method_response", config.ADD_RESPONSE_DEFAULT) or "refresh"   143    144     def get_offer_period(self):   145    146         "Decode a specification in the iCalendar duration format."   147    148         prefs = self.get_preferences()   149         duration = prefs and prefs.get("freebusy_offers", config.FREEBUSY_OFFER_DEFAULT)   150    151         # NOTE: Should probably report an error somehow if None.   152    153         return duration and get_duration(duration) or None   154    155     def get_organiser_replacement(self):   156         prefs = self.get_preferences()   157         return prefs and prefs.get("organiser_replacement", config.ORGANISER_REPLACEMENT_DEFAULT) or "attendee"   158    159     def have_manager(self):   160         return config.MANAGER_INTERFACE   161    162     def get_permitted_values(self):   163    164         """   165         Decode a specification of one of the following forms...   166    167         <minute values>   168         <hour values>:<minute values>   169         <hour values>:<minute values>:<second values>   170    171         ...with each list of values being comma-separated.   172         """   173    174         prefs = self.get_preferences()   175         permitted_values = prefs and prefs.get("permitted_times")   176         if permitted_values:   177             try:   178                 l = []   179                 for component in permitted_values.split(":")[:3]:   180                     if component:   181                         l.append(map(int, component.split(",")))   182                     else:   183                         l.append(None)   184    185             # NOTE: Should probably report an error somehow.   186    187             except ValueError:   188                 return None   189             else:   190                 l = (len(l) < 2 and [None] or []) + l + (len(l) < 3 and [None] or [])   191                 return l   192         else:   193             return None   194    195     # Common operations on calendar data.   196    197     def update_attendees(self, obj, attendees, removed):   198    199         """   200         Update the attendees in 'obj' with the given 'attendees' and 'removed'   201         attendee lists. A list is returned containing the attendees whose   202         attendance should be cancelled.   203         """   204    205         to_cancel = []   206    207         existing_attendees = uri_values(obj.get_values("ATTENDEE") or [])   208         added = set(attendees).difference(existing_attendees)   209    210         if added or removed:   211             attendees = uri_items(obj.get_items("ATTENDEE") or [])   212             sequence = obj.get_value("SEQUENCE")   213    214             if removed:   215                 remaining = []   216    217                 for attendee, attendee_attr in attendees:   218                     if attendee in removed:   219    220                         # Without a sequence number, assume that the event has not   221                         # been published and that attendees can be silently removed.   222    223                         if sequence is not None:   224                             to_cancel.append((attendee, attendee_attr))   225                     else:   226                         remaining.append((attendee, attendee_attr))   227    228                 attendees = remaining   229    230             if added:   231                 for attendee in added:   232                     attendee = attendee.strip()   233                     if attendee:   234                         attendees.append((get_uri(attendee), {"PARTSTAT" : "NEEDS-ACTION", "RSVP" : "TRUE"}))   235    236             obj["ATTENDEE"] = attendees   237    238         return to_cancel   239    240     def update_participation(self, obj, partstat=None):   241    242         """   243         Update the participation in 'obj' of the user with the given 'partstat'.   244         """   245    246         attendee_attr = uri_dict(obj.get_value_map("ATTENDEE")).get(self.user)   247         if not attendee_attr:   248             return None   249         if partstat:   250             attendee_attr["PARTSTAT"] = partstat   251         if attendee_attr.has_key("RSVP"):   252             del attendee_attr["RSVP"]   253         self.update_sender(attendee_attr)   254         return attendee_attr   255    256     def update_sender(self, attr):   257    258         "Update the SENT-BY attribute of the 'attr' sender metadata."   259    260         if self.messenger and self.messenger.sender != get_address(self.user):   261             attr["SENT-BY"] = get_uri(self.messenger.sender)   262    263     def get_periods(self, obj):   264    265         """   266         Return periods for the given 'obj'. Interpretation of periods can depend   267         on the time zone, which is obtained for the current user.   268         """   269    270         return obj.get_periods(self.get_tzid(), self.get_window_end())   271    272     # Store operations.   273    274     def get_stored_object(self, uid, recurrenceid, section=None, username=None):   275    276         """   277         Return the stored object for the current user, with the given 'uid' and   278         'recurrenceid' from the given 'section' and for the given 'username' (if   279         specified), or from the standard object collection otherwise.   280         """   281    282         if section == "counters":   283             fragment = self.store.get_counter(self.user, username, uid, recurrenceid)   284         else:   285             fragment = self.store.get_event(self.user, uid, recurrenceid)   286         return fragment and Object(fragment)   287    288     # Free/busy operations.   289    290     def get_freebusy_part(self, freebusy=None):   291    292         """   293         Return a message part containing free/busy information for the user,   294         either specified as 'freebusy' or obtained from the store directly.   295         """   296    297         if self.is_sharing() and self.is_bundling():   298    299             # Invent a unique identifier.   300    301             utcnow = get_timestamp()   302             uid = "imip-agent-%s-%s" % (utcnow, get_address(self.user))   303    304             freebusy = freebusy or self.store.get_freebusy(self.user)   305    306             user_attr = {}   307             self.update_sender(user_attr)   308             return to_part("PUBLISH", [make_freebusy(freebusy, uid, self.user, user_attr)])   309    310         return None   311    312     def update_freebusy(self, freebusy, periods, transp, uid, recurrenceid, summary, organiser, expires=None):   313    314         """   315         Update the 'freebusy' collection with the given 'periods', indicating a   316         'transp' status, explicit 'uid' and 'recurrenceid' to indicate either a   317         recurrence or the parent event. The 'summary' and 'organiser' must also   318         be provided.   319    320         An optional 'expires' datetime string can be provided to tag a free/busy   321         offer.   322         """   323    324         update_freebusy(freebusy, periods, transp, uid, recurrenceid, summary, organiser, expires)   325    326 class ClientForObject(Client):   327    328     "A client maintaining a specific object."   329    330     def __init__(self, obj, user, messenger=None, store=None, publisher=None, preferences_dir=None):   331         Client.__init__(self, user, messenger, store, publisher, preferences_dir)   332         self.set_object(obj)   333    334     def set_object(self, obj):   335    336         "Set the current object to 'obj', obtaining metadata details."   337    338         self.obj = obj   339         self.uid = obj and self.obj.get_uid()   340         self.recurrenceid = obj and self.obj.get_recurrenceid()   341         self.sequence = obj and self.obj.get_value("SEQUENCE")   342         self.dtstamp = obj and self.obj.get_value("DTSTAMP")   343    344     def set_identity(self, method):   345    346         """   347         Set the current user for the current object in the context of the given   348         'method'. It is usually set when initialising the handler, using the   349         recipient details, but outgoing messages do not reference the recipient   350         in this way.   351         """   352    353         pass   354    355     def is_usable(self, method=None):   356    357         "Return whether the current object is usable with the given 'method'."   358    359         return True   360    361     # Object update methods.   362    363     def update_recurrenceid(self):   364    365         """   366         Update the RECURRENCE-ID in the current object, initialising it from   367         DTSTART.   368         """   369    370         self.obj["RECURRENCE-ID"] = [self.obj.get_item("DTSTART")]   371         self.recurrenceid = self.obj.get_recurrenceid()   372    373     def update_dtstamp(self):   374    375         "Update the DTSTAMP in the current object."   376    377         dtstamp = self.obj.get_utc_datetime("DTSTAMP")   378         utcnow = get_time()   379         self.dtstamp = format_datetime(dtstamp and dtstamp > utcnow and dtstamp or utcnow)   380         self.obj["DTSTAMP"] = [(self.dtstamp, {})]   381    382     def set_sequence(self, increment=False):   383    384         "Update the SEQUENCE in the current object."   385    386         sequence = self.obj.get_value("SEQUENCE") or "0"   387         self.obj["SEQUENCE"] = [(str(int(sequence) + (increment and 1 or 0)), {})]   388    389     def merge_attendance(self, attendees):   390    391         """   392         Merge attendance from the current object's 'attendees' into the version   393         stored for the current user.   394         """   395    396         obj = self.get_stored_object_version()   397    398         if not obj or not self.have_new_object():   399             return False   400    401         # Get attendee details in a usable form.   402    403         attendee_map = uri_dict(obj.get_value_map("ATTENDEE"))   404    405         for attendee, attendee_attr in attendees.items():   406    407             # Update attendance in the loaded object.   408    409             attendee_map[attendee] = attendee_attr   410    411         # Set the new details and store the object.   412    413         obj["ATTENDEE"] = attendee_map.items()   414    415         # Set a specific recurrence or the complete event if not an additional   416         # occurrence.   417    418         self.store.set_event(self.user, self.uid, self.recurrenceid, obj.to_node())   419    420         return True   421    422     # Object-related tests.   423    424     def is_recognised_organiser(self, organiser):   425    426         """   427         Return whether the given 'organiser' is recognised from   428         previously-received details. If no stored details exist, True is   429         returned.   430         """   431    432         obj = self.get_stored_object_version()   433         if obj:   434             stored_organiser = get_uri(obj.get_value("ORGANIZER"))   435             return stored_organiser == organiser   436         else:   437             return True   438    439     def is_recognised_attendee(self, attendee):   440    441         """   442         Return whether the given 'attendee' is recognised from   443         previously-received details. If no stored details exist, True is   444         returned.   445         """   446    447         obj = self.get_stored_object_version()   448         if obj:   449             stored_attendees = uri_dict(obj.get_value_map("ATTENDEE"))   450             return stored_attendees.has_key(attendee)   451         else:   452             return True   453    454     def get_attendance(self, user=None, obj=None):   455    456         """   457         Return the attendance attributes for 'user', or the current user if   458         'user' is not specified.   459         """   460    461         attendees = uri_dict((obj or self.obj).get_value_map("ATTENDEE"))   462         return attendees.get(user or self.user)   463    464     def is_participating(self, user, as_organiser=False, obj=None):   465    466         """   467         Return whether, subject to the 'user' indicating an identity and the   468         'as_organiser' status of that identity, the user concerned is actually   469         participating in the current object event.   470         """   471    472         # Use any attendee property information for an organiser, not the   473         # organiser property attributes.   474    475         attr = self.get_attendance(user, obj=obj)   476         return as_organiser or attr is not None and not attr or attr and attr.get("PARTSTAT") != "DECLINED"   477    478     def get_overriding_transparency(self, user, as_organiser=False):   479    480         """   481         Return the overriding transparency to be associated with the free/busy   482         records for an event, subject to the 'user' indicating an identity and   483         the 'as_organiser' status of that identity.   484    485         Where an identity is only an organiser and not attending, "ORG" is   486         returned. Otherwise, no overriding transparency is defined and None is   487         returned.   488         """   489    490         attr = self.get_attendance(user)   491         return as_organiser and not (attr and attr.get("PARTSTAT")) and "ORG" or None   492    493     def can_schedule(self, freebusy, periods):   494    495         """   496         Indicate whether within 'freebusy' the given 'periods' can be scheduled.   497         """   498    499         return can_schedule(freebusy, periods, self.uid, self.recurrenceid)   500    501     def have_new_object(self, strict=True):   502    503         """   504         Return whether the current object is new to the current user.   505    506         If 'strict' is specified and is a false value, the DTSTAMP test will be   507         ignored. This is useful in handling responses from attendees from   508         clients (like Claws Mail) that erase time information from DTSTAMP and   509         make it invalid.   510         """   511    512         obj = self.get_stored_object_version()   513    514         # If found, compare SEQUENCE and potentially DTSTAMP.   515    516         if obj:   517             sequence = obj.get_value("SEQUENCE")   518             dtstamp = obj.get_value("DTSTAMP")   519    520             # If the request refers to an older version of the object, ignore   521             # it.   522    523             return is_new_object(sequence, self.sequence, dtstamp, self.dtstamp, not strict)   524    525         return True   526    527     def possibly_recurring_indefinitely(self):   528    529         "Return whether the object recurs indefinitely."   530    531         # Obtain the stored object to make sure that recurrence information   532         # is not being ignored. This might happen if a client sends a   533         # cancellation without the complete set of properties, for instance.   534    535         return self.obj.possibly_recurring_indefinitely() or \   536                self.get_stored_object_version() and \   537                self.get_stored_object_version().possibly_recurring_indefinitely()   538    539     # Constraint application on event periods.   540    541     def check_object(self):   542    543         "Check the object against any scheduling constraints."   544    545         permitted_values = self.get_permitted_values()   546         if not permitted_values:   547             return None   548    549         invalid = []   550    551         for period in self.obj.get_periods(self.get_tzid()):   552             start = period.get_start()   553             end = period.get_end()   554             start_errors = check_permitted_values(start, permitted_values)   555             end_errors = check_permitted_values(end, permitted_values)   556             if start_errors or end_errors:   557                 invalid.append((period.origin, start_errors, end_errors))   558    559         return invalid   560    561     def correct_object(self):   562    563         "Correct the object according to any scheduling constraints."   564    565         permitted_values = self.get_permitted_values()   566         return permitted_values and self.obj.correct_object(self.get_tzid(), permitted_values)   567    568     # Object retrieval.   569    570     def get_stored_object_version(self):   571    572         """   573         Return the stored object to which the current object refers for the   574         current user.   575         """   576    577         return self.get_stored_object(self.uid, self.recurrenceid)   578    579     def get_definitive_object(self, as_organiser):   580    581         """   582         Return an object considered definitive for the current transaction,   583         using 'as_organiser' to select the current transaction's object if   584         false, or selecting a stored object if true.   585         """   586    587         return not as_organiser and self.obj or self.get_stored_object_version()   588    589     def get_parent_object(self):   590    591         """   592         Return the parent object to which the current object refers for the   593         current user.   594         """   595    596         return self.recurrenceid and self.get_stored_object(self.uid, None) or None   597    598     # Convenience methods for modifying free/busy collections.   599    600     def get_recurrence_start_point(self, recurrenceid):   601    602         "Get 'recurrenceid' in a form suitable for matching free/busy entries."   603    604         return self.obj.get_recurrence_start_point(recurrenceid, self.get_tzid())   605    606     def remove_from_freebusy(self, freebusy):   607    608         "Remove this event from the given 'freebusy' collection."   609    610         if not remove_period(freebusy, self.uid, self.recurrenceid) and self.recurrenceid:   611             remove_affected_period(freebusy, self.uid, self.get_recurrence_start_point(self.recurrenceid))   612    613     def remove_freebusy_for_recurrences(self, freebusy, recurrenceids=None):   614    615         """   616         Remove from 'freebusy' any original recurrence from parent free/busy   617         details for the current object, if the current object is a specific   618         additional recurrence. Otherwise, remove all additional recurrence   619         information corresponding to 'recurrenceids', or if omitted, all   620         recurrences.   621         """   622    623         if self.recurrenceid:   624             recurrenceid = self.get_recurrence_start_point(self.recurrenceid)   625             remove_affected_period(freebusy, self.uid, recurrenceid)   626         else:   627             # Remove obsolete recurrence periods.   628    629             remove_additional_periods(freebusy, self.uid, recurrenceids)   630    631             # Remove original periods affected by additional recurrences.   632    633             if recurrenceids:   634                 for recurrenceid in recurrenceids:   635                     recurrenceid = self.get_recurrence_start_point(recurrenceid)   636                     remove_affected_period(freebusy, self.uid, recurrenceid)   637    638     def update_freebusy(self, freebusy, user, as_organiser, offer=False):   639    640         """   641         Update the 'freebusy' collection for this event with the periods and   642         transparency associated with the current object, subject to the 'user'   643         identity and the attendance details provided for them, indicating   644         whether the update is being done 'as_organiser' (for the organiser of   645         an event) or not.   646    647         If 'offer' is set to a true value, any free/busy updates will be tagged   648         with an expiry time.   649         """   650    651         # Obtain the stored object if the current object is not issued by the   652         # organiser. Attendees do not have the opportunity to redefine the   653         # periods.   654    655         obj = self.get_definitive_object(as_organiser)   656         if not obj:   657             return   658    659         # Obtain the affected periods.   660    661         periods = self.get_periods(obj)   662    663         # Define an overriding transparency, the indicated event transparency,   664         # or the default transparency for the free/busy entry.   665    666         transp = self.get_overriding_transparency(user, as_organiser) or \   667                  obj.get_value("TRANSP") or \   668                  "OPAQUE"   669    670         # Calculate any expiry time. If no offer period is defined, do not   671         # record the offer periods.   672    673         if offer:   674             offer_period = self.get_offer_period()   675             if offer_period:   676                 expires = get_timestamp(offer_period)   677             else:   678                 return   679         else:   680             expires = None   681    682         # Perform the low-level update.   683    684         Client.update_freebusy(self, freebusy, periods, transp,   685             self.uid, self.recurrenceid,   686             obj.get_value("SUMMARY"),   687             obj.get_value("ORGANIZER"),   688             expires)   689    690     def update_freebusy_for_participant(self, freebusy, user, for_organiser=False,   691                                         updating_other=False, offer=False):   692    693         """   694         Update the 'freebusy' collection for the given 'user', indicating   695         whether the update is 'for_organiser' (being done for the organiser of   696         an event) or not, and whether it is 'updating_other' (meaning another   697         user's details).   698    699         If 'offer' is set to a true value, any free/busy updates will be tagged   700         with an expiry time.   701         """   702    703         # Record in the free/busy details unless a non-participating attendee.   704         # Remove periods for non-participating attendees.   705    706         if offer or self.is_participating(user, for_organiser and not updating_other):   707             self.update_freebusy(freebusy, user,   708                 for_organiser and not updating_other or   709                 not for_organiser and updating_other,   710                 offer   711                 )   712         else:   713             self.remove_from_freebusy(freebusy)   714    715     def remove_freebusy_for_participant(self, freebusy, user, for_organiser=False,   716                                         updating_other=False):   717    718         """   719         Remove details from the 'freebusy' collection for the given 'user',   720         indicating whether the modification is 'for_organiser' (being done for   721         the organiser of an event) or not, and whether it is 'updating_other'   722         (meaning another user's details).   723         """   724    725         # Remove from the free/busy details if a specified attendee.   726    727         if self.is_participating(user, for_organiser and not updating_other):   728             self.remove_from_freebusy(freebusy)   729    730     # Convenience methods for updating stored free/busy information received   731     # from other users.   732    733     def update_freebusy_from_participant(self, user, for_organiser, fn=None):   734    735         """   736         For the current user, record the free/busy information for another   737         'user', indicating whether the update is 'for_organiser' or not, thus   738         maintaining a separate record of their free/busy details.   739         """   740    741         fn = fn or self.update_freebusy_for_participant   742    743         # A user does not store free/busy information for themself as another   744         # party.   745    746         if user == self.user:   747             return   748    749         self.acquire_lock()   750         try:   751             freebusy = self.store.get_freebusy_for_other(self.user, user)   752             fn(freebusy, user, for_organiser, True)   753    754             # Tidy up any obsolete recurrences.   755    756             self.remove_freebusy_for_recurrences(freebusy, self.store.get_recurrences(self.user, self.uid))   757             self.store.set_freebusy_for_other(self.user, freebusy, user)   758    759         finally:   760             self.release_lock()   761    762     def update_freebusy_from_organiser(self, organiser):   763    764         "For the current user, record free/busy information from 'organiser'."   765    766         self.update_freebusy_from_participant(organiser, True)   767    768     def update_freebusy_from_attendees(self, attendees):   769    770         "For the current user, record free/busy information from 'attendees'."   771    772         for attendee in attendees.keys():   773             self.update_freebusy_from_participant(attendee, False)   774    775     def remove_freebusy_from_organiser(self, organiser):   776    777         "For the current user, remove free/busy information from 'organiser'."   778    779         self.update_freebusy_from_participant(organiser, True, self.remove_freebusy_for_participant)   780    781     def remove_freebusy_from_attendees(self, attendees):   782    783         "For the current user, remove free/busy information from 'attendees'."   784    785         for attendee in attendees.keys():   786             self.update_freebusy_from_participant(attendee, False, self.remove_freebusy_for_participant)   787    788     # Convenience methods for updating free/busy details at the event level.   789    790     def update_event_in_freebusy(self, for_organiser=True):   791    792         """   793         Update free/busy information when handling an object, doing so for the   794         organiser of an event if 'for_organiser' is set to a true value.   795         """   796    797         freebusy = self.store.get_freebusy(self.user)   798    799         # Obtain the attendance attributes for this user, if available.   800    801         self.update_freebusy_for_participant(freebusy, self.user, for_organiser)   802    803         # Remove original recurrence details replaced by additional   804         # recurrences, as well as obsolete additional recurrences.   805    806         self.remove_freebusy_for_recurrences(freebusy, self.store.get_recurrences(self.user, self.uid))   807         self.store.set_freebusy(self.user, freebusy)   808    809         if self.publisher and self.is_sharing() and self.is_publishing():   810             self.publisher.set_freebusy(self.user, freebusy)   811    812         # Update free/busy provider information if the event may recur   813         # indefinitely.   814    815         if self.possibly_recurring_indefinitely():   816             self.store.append_freebusy_provider(self.user, self.obj)   817    818         return True   819    820     def remove_event_from_freebusy(self):   821    822         "Remove free/busy information when handling an object."   823    824         freebusy = self.store.get_freebusy(self.user)   825    826         self.remove_from_freebusy(freebusy)   827         self.remove_freebusy_for_recurrences(freebusy)   828         self.store.set_freebusy(self.user, freebusy)   829    830         if self.publisher and self.is_sharing() and self.is_publishing():   831             self.publisher.set_freebusy(self.user, freebusy)   832    833         # Update free/busy provider information if the event may recur   834         # indefinitely.   835    836         if self.possibly_recurring_indefinitely():   837             self.store.remove_freebusy_provider(self.user, self.obj)   838    839     def update_event_in_freebusy_offers(self):   840    841         "Update free/busy offers when handling an object."   842    843         freebusy = self.store.get_freebusy_offers(self.user)   844    845         # Obtain the attendance attributes for this user, if available.   846    847         self.update_freebusy_for_participant(freebusy, self.user, offer=True)   848    849         # Remove original recurrence details replaced by additional   850         # recurrences, as well as obsolete additional recurrences.   851    852         self.remove_freebusy_for_recurrences(freebusy, self.store.get_recurrences(self.user, self.uid))   853         self.store.set_freebusy_offers(self.user, freebusy)   854    855         return True   856    857     def remove_event_from_freebusy_offers(self):   858    859         "Remove free/busy offers when handling an object."   860    861         freebusy = self.store.get_freebusy_offers(self.user)   862    863         self.remove_from_freebusy(freebusy)   864         self.remove_freebusy_for_recurrences(freebusy)   865         self.store.set_freebusy_offers(self.user, freebusy)   866    867         return True   868    869 # vim: tabstop=4 expandtab shiftwidth=4