imip-agent

Annotated imiptools/handlers/resource.py

1339:b839e8ad5f77
2017-10-17 Paul Boddie Added notes about local Unix mailbox message storage.
paul@48 1
#!/usr/bin/env python
paul@48 2
paul@48 3
"""
paul@48 4
Handlers for a resource.
paul@146 5
paul@1263 6
Copyright (C) 2014, 2015, 2016, 2017 Paul Boddie <paul@boddie.org.uk>
paul@146 7
paul@146 8
This program is free software; you can redistribute it and/or modify it under
paul@146 9
the terms of the GNU General Public License as published by the Free Software
paul@146 10
Foundation; either version 3 of the License, or (at your option) any later
paul@146 11
version.
paul@146 12
paul@146 13
This program is distributed in the hope that it will be useful, but WITHOUT
paul@146 14
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
paul@146 15
FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
paul@146 16
details.
paul@146 17
paul@146 18
You should have received a copy of the GNU General Public License along with
paul@146 19
this program.  If not, see <http://www.gnu.org/licenses/>.
paul@48 20
"""
paul@48 21
paul@1153 22
from email.mime.text import MIMEText
paul@1336 23
from imiptools.data import get_address
paul@418 24
from imiptools.handlers import Handler
paul@683 25
from imiptools.handlers.common import CommonFreebusy, CommonEvent
paul@1039 26
from imiptools.handlers.scheduling import apply_scheduling_functions, \
paul@1040 27
                                          confirm_scheduling, \
paul@1040 28
                                          finish_scheduling, \
paul@1040 29
                                          retract_scheduling
paul@48 30
paul@725 31
class ResourceHandler(CommonEvent, Handler):
paul@131 32
paul@131 33
    "Handling mechanisms specific to resources."
paul@131 34
paul@937 35
    def _process(self, handle_for_attendee):
paul@131 36
paul@420 37
        """
paul@420 38
        Record details from the incoming message, using the given
paul@420 39
        'handle_for_attendee' callable to process any valid message
paul@420 40
        appropriately.
paul@420 41
        """
paul@420 42
paul@131 43
        oa = self.require_organiser_and_attendees()
paul@131 44
        if not oa:
paul@131 45
            return None
paul@131 46
paul@131 47
        organiser_item, attendees = oa
paul@131 48
paul@468 49
        # Process for the current user, a resource as attendee.
paul@131 50
paul@738 51
        if not self.have_new_object():
paul@468 52
            return None
paul@131 53
paul@468 54
        # Collect response objects produced when handling the request.
paul@131 55
paul@662 56
        handle_for_attendee()
paul@131 57
paul@676 58
    def _add_for_attendee(self):
paul@131 59
paul@420 60
        """
paul@676 61
        Attempt to add a recurrence to an existing object for the current user.
paul@676 62
        This does not request a response concerning participation, apparently.
paul@420 63
        """
paul@420 64
paul@737 65
        # Request details where configured, doing so for unknown objects anyway.
paul@676 66
paul@737 67
        if self.will_refresh():
paul@737 68
            self.make_refresh()
paul@676 69
            return
paul@676 70
paul@676 71
        # Record the event as a recurrence of the parent object.
paul@676 72
paul@676 73
        self.update_recurrenceid()
paul@737 74
        self.store.set_event(self.user, self.uid, self.recurrenceid, self.obj.to_node())
paul@676 75
paul@859 76
        # Remove any previous cancellations involving this event.
paul@859 77
paul@859 78
        self.store.remove_cancellation(self.user, self.uid, self.recurrenceid)
paul@859 79
paul@676 80
        # Update free/busy information.
paul@676 81
paul@676 82
        self.update_event_in_freebusy(for_organiser=False)
paul@676 83
paul@1039 84
        # Confirm the scheduling of the recurrence.
paul@1039 85
paul@1039 86
        self.confirm_scheduling()
paul@1039 87
paul@676 88
    def _schedule_for_attendee(self):
paul@676 89
paul@676 90
        "Attempt to schedule the current object for the current user."
paul@676 91
paul@1336 92
        attendee_attr = self.obj.get_uri_map("ATTENDEE")[self.user]
paul@1177 93
        delegates = None
paul@1040 94
paul@1040 95
        # Attempt to schedule the event.
paul@1040 96
paul@1191 97
        try:
paul@1191 98
            scheduled, description = self.schedule()
paul@655 99
paul@1040 100
            # Update the participation of the resource in the object.
paul@1040 101
            # Update free/busy information.
paul@662 102
paul@1040 103
            if scheduled in ("ACCEPTED", "DECLINED"):
paul@1040 104
                method = "REPLY"
paul@1040 105
                attendee_attr = self.update_participation(scheduled)
paul@662 106
paul@1040 107
                # Set the complete event or an additional occurrence.
paul@334 108
paul@1040 109
                event = self.obj.to_node()
paul@1040 110
                self.store.set_event(self.user, self.uid, self.recurrenceid, event)
paul@381 111
paul@1040 112
                # Remove additional recurrences if handling a complete event.
paul@1040 113
                # Also remove any previous cancellations involving this event.
paul@381 114
paul@1040 115
                if not self.recurrenceid:
paul@1040 116
                    self.store.remove_recurrences(self.user, self.uid)
paul@1040 117
                    self.store.remove_cancellations(self.user, self.uid)
paul@1040 118
                else:
paul@1040 119
                    self.store.remove_cancellation(self.user, self.uid, self.recurrenceid)
paul@361 120
paul@1263 121
                # Update free/busy details only after recurrences have been
paul@1263 122
                # removed, where appropriate.
paul@1263 123
paul@1263 124
                self.update_event_in_freebusy(for_organiser=False)
paul@1263 125
                self.remove_event_from_freebusy_offers()
paul@1263 126
paul@1040 127
                if scheduled == "ACCEPTED":
paul@1040 128
                    self.confirm_scheduling()
paul@1039 129
paul@1177 130
            # For delegated proposals, prepare a request to the delegates in
paul@1176 131
            # addition to the usual response.
paul@1176 132
paul@1176 133
            elif scheduled == "DELEGATED":
paul@1176 134
                method = "REPLY"
paul@1176 135
                attendee_attr = self.update_participation("DELEGATED")
paul@1176 136
paul@1176 137
                # The recipient will have indicated the delegate whose details
paul@1176 138
                # will have been added to the object.
paul@1176 139
paul@1177 140
                delegates = attendee_attr["DELEGATED-TO"]
paul@1176 141
paul@1040 142
            # For countered proposals, record the offer in the resource's
paul@1040 143
            # free/busy collection.
paul@1039 144
paul@1040 145
            elif scheduled == "COUNTER":
paul@1040 146
                method = "COUNTER"
paul@1040 147
                self.update_event_in_freebusy_offers()
paul@1040 148
paul@1040 149
            # For inappropriate periods, reply declining participation.
paul@936 150
paul@1040 151
            else:
paul@1040 152
                method = "REPLY"
paul@1040 153
                attendee_attr = self.update_participation("DECLINED")
paul@936 154
paul@1040 155
        # Confirm any scheduling.
paul@936 156
paul@1040 157
        finally:
paul@1040 158
            self.finish_scheduling()
paul@936 159
paul@1176 160
        # Determine the recipients of the outgoing messages.
paul@1176 161
paul@1153 162
        recipients = map(get_address, self.obj.get_values("ORGANIZER"))
paul@1153 163
paul@1153 164
        # Add any description of the scheduling decision.
paul@1153 165
paul@1153 166
        self.add_result(None, recipients, MIMEText(description))
paul@1153 167
paul@580 168
        # Make a version of the object with just this attendee, update the
paul@580 169
        # DTSTAMP in the response, and return the object for sending.
paul@574 170
paul@745 171
        self.update_sender(attendee_attr)
paul@1176 172
        attendees = [(self.user, attendee_attr)]
paul@1176 173
paul@1177 174
        # Add delegates if delegating (RFC 5546 being inconsistent here since
paul@1176 175
        # it provides an example reply to the organiser without the delegate).
paul@1176 176
paul@1177 177
        if delegates:
paul@1177 178
            for delegate in delegates:
paul@1336 179
                delegate_attr = self.obj.get_uri_map("ATTENDEE")[delegate]
paul@1177 180
                attendees.append((delegate, delegate_attr))
paul@1176 181
paul@1176 182
        # Reply to the delegator in addition to the organiser if replying to a
paul@1176 183
        # delegation request.
paul@1176 184
paul@1177 185
        delegators = self.is_delegation()
paul@1177 186
        if delegators:
paul@1177 187
            for delegator in delegators:
paul@1336 188
                delegator_attr = self.obj.get_uri_map("ATTENDEE")[delegator]
paul@1177 189
                attendees.append((delegator, delegator_attr))
paul@1177 190
                recipients.append(get_address(delegator))
paul@1176 191
paul@1176 192
        # Prepare the response for the organiser plus any delegator.
paul@1176 193
paul@1176 194
        self.obj["ATTENDEE"] = attendees
paul@574 195
        self.update_dtstamp()
paul@1176 196
        self.add_result(method, recipients, self.object_to_part(method, self.obj))
paul@1176 197
paul@1177 198
        # If delegating, send a request to the delegates.
paul@1176 199
paul@1177 200
        if delegates:
paul@1176 201
            method = "REQUEST"
paul@1177 202
            self.add_result(method, map(get_address, delegates), self.object_to_part(method, self.obj))
paul@131 203
paul@468 204
    def _cancel_for_attendee(self):
paul@131 205
paul@420 206
        """
paul@468 207
        Cancel for the current user their attendance of the event described by
paul@468 208
        the current object.
paul@420 209
        """
paul@420 210
paul@580 211
        # Update free/busy information.
paul@131 212
paul@580 213
        self.remove_event_from_freebusy()
paul@580 214
paul@672 215
        # Update the stored event and cancel it.
paul@672 216
paul@672 217
        self.store.set_event(self.user, self.uid, self.recurrenceid, self.obj.to_node())
paul@672 218
        self.store.cancel_event(self.user, self.uid, self.recurrenceid)
paul@672 219
paul@1039 220
        # Retract the scheduling of the event.
paul@1039 221
paul@1039 222
        self.retract_scheduling()
paul@1039 223
paul@710 224
    def _revoke_for_attendee(self):
paul@710 225
paul@710 226
        "Revoke any counter-proposal recorded as a free/busy offer."
paul@710 227
paul@710 228
        self.remove_event_from_freebusy_offers()
paul@710 229
paul@936 230
    # Scheduling details.
paul@936 231
paul@1058 232
    def get_scheduling_functions(self):
paul@1058 233
paul@1058 234
        "Return the scheduling functions for the resource."
paul@1058 235
paul@1058 236
        return self.get_preferences().get("scheduling_function",
paul@1058 237
            "schedule_in_freebusy").split("\n")
paul@1058 238
paul@936 239
    def schedule(self):
paul@936 240
paul@936 241
        """
paul@936 242
        Attempt to schedule the current object, returning an indication of the
paul@936 243
        kind of response to be returned: "COUNTER" for counter-proposals,
paul@936 244
        "ACCEPTED" for acceptances, "DECLINED" for rejections, and None for
paul@936 245
        invalid requests.
paul@936 246
        """
paul@936 247
paul@1058 248
        return apply_scheduling_functions(self)
paul@936 249
paul@1039 250
    def confirm_scheduling(self):
paul@1039 251
paul@1039 252
        "Confirm that this event has been scheduled."
paul@1039 253
paul@1058 254
        confirm_scheduling(self)
paul@1039 255
paul@1040 256
    def finish_scheduling(self):
paul@1040 257
paul@1040 258
        "Finish the scheduling, unlocking resources where appropriate."
paul@1040 259
paul@1058 260
        finish_scheduling(self)
paul@1040 261
paul@1039 262
    def retract_scheduling(self):
paul@1039 263
paul@1039 264
        "Retract this event from scheduling records."
paul@1039 265
paul@1058 266
        retract_scheduling(self)
paul@1039 267
paul@131 268
class Event(ResourceHandler):
paul@48 269
paul@48 270
    "An event handler."
paul@48 271
paul@48 272
    def add(self):
paul@676 273
paul@676 274
        "Add a new occurrence to an existing event."
paul@676 275
paul@937 276
        self._process(self._add_for_attendee)
paul@48 277
paul@48 278
    def cancel(self):
paul@131 279
paul@131 280
        "Cancel attendance for attendees."
paul@131 281
paul@937 282
        self._process(self._cancel_for_attendee)
paul@48 283
paul@48 284
    def counter(self):
paul@48 285
paul@48 286
        "Since this handler does not send requests, it will not handle replies."
paul@48 287
paul@48 288
        pass
paul@48 289
paul@48 290
    def declinecounter(self):
paul@48 291
paul@710 292
        "Revoke any counter-proposal."
paul@48 293
paul@937 294
        self._process(self._revoke_for_attendee)
paul@48 295
paul@48 296
    def publish(self):
paul@676 297
paul@676 298
        """
paul@676 299
        Resources only consider events sent as requests, not generally published
paul@676 300
        events.
paul@676 301
        """
paul@676 302
paul@48 303
        pass
paul@48 304
paul@48 305
    def refresh(self):
paul@626 306
paul@626 307
        """
paul@626 308
        Refresh messages are typically sent to event organisers, but resources
paul@626 309
        do not act as organisers themselves.
paul@626 310
        """
paul@48 311
paul@676 312
        pass
paul@676 313
paul@48 314
    def reply(self):
paul@48 315
paul@48 316
        "Since this handler does not send requests, it will not handle replies."
paul@48 317
paul@48 318
        pass
paul@48 319
paul@48 320
    def request(self):
paul@48 321
paul@48 322
        """
paul@48 323
        Respond to a request by preparing a reply containing accept/decline
paul@468 324
        information for the recipient.
paul@48 325
paul@48 326
        No support for countering requests is implemented.
paul@48 327
        """
paul@48 328
paul@937 329
        self._process(self._schedule_for_attendee)
paul@48 330
paul@725 331
class Freebusy(CommonFreebusy, Handler):
paul@48 332
paul@48 333
    "A free/busy handler."
paul@48 334
paul@48 335
    def publish(self):
paul@676 336
paul@676 337
        "Resources ignore generally published free/busy information."
paul@676 338
paul@943 339
        self._record_freebusy(from_organiser=True)
paul@48 340
paul@48 341
    def reply(self):
paul@48 342
paul@48 343
        "Since this handler does not send requests, it will not handle replies."
paul@48 344
paul@48 345
        pass
paul@48 346
paul@108 347
    # request provided by CommonFreeBusy.request
paul@48 348
paul@48 349
# Handler registry.
paul@48 350
paul@48 351
handlers = [
paul@48 352
    ("VFREEBUSY",   Freebusy),
paul@48 353
    ("VEVENT",      Event),
paul@48 354
    ]
paul@48 355
paul@48 356
# vim: tabstop=4 expandtab shiftwidth=4