1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233
|
import sys
import uuid
from datetime import datetime
from datetime import timedelta
import pytz
from caldav import DAVClient
from caldav import error
from icalendar import Calendar
from icalendar import Event
###############
### SETUP START
### rfc6638_users should be a list with three dicts containing credential details.
### if none is given, attempt to use three test users on tobixens private calendar
###
try:
from tests.conf_private import rfc6638_users
except:
rfc6638_users = None
## Some initial setup. We'll need three caldav client objects, with
## corresponding principal objects and calendars.
class TestUser:
def __init__(self, i):
if rfc6638_users and len(rfc6638_users) > i - 1:
conndata = rfc6638_users[i - 1].copy()
if "incompatibilities" in conndata:
conndata.pop("incompatibilities")
self.client = DAVClient(**conndata)
else:
self.client = DAVClient(
username="testuser%i" % i,
password="testpass%i" % i,
url="http://calendar.tobixen.no/caldav.php/",
)
self.principal = self.client.principal()
calendar_id = "schedulingtestcalendar%i" % i
calendar_name = "calendar #%i for scheduling demo" % i
self.cleanup(calendar_name)
self.calendar = self.principal.make_calendar(
name=calendar_name, cal_id=calendar_id
)
def cleanup(self, calendar_name):
## Cleanup from earlier runs
try:
self.calendar = self.principal.calendar(name=calendar_name)
self.calendar.delete()
except error.NotFoundError:
pass
## Hmm ... perhaps we shouldn't delete inbox items
# for inbox_item in self.principal.schedule_inbox().get_items():
# inbox_item.delete()
organizer = TestUser(1)
attendee1 = TestUser(2)
attendee2 = TestUser(3)
### SETUP END
###############
## Verify that the calendar server(s) supports scheduling
for test_user in organizer, attendee1, attendee2:
if not test_user.client.check_scheduling_support():
print("Server does not support RFC6638")
sys.exit(1)
## We'll be using the icalendar library to set up a mock meeting,
## at some far point in the future.
caldata = Calendar()
caldata.add("prodid", "-//tobixen//python-icalendar//en_DK")
caldata.add("version", "2.0")
uid = uuid.uuid1()
event = Event()
event.add("dtstamp", datetime.now())
event.add("dtstart", datetime.now() + timedelta(days=4000))
event.add("dtend", datetime.now() + timedelta(days=4000, hours=1))
event.add("uid", uid)
event.add("summary", "Some test event made to test scheduling in the caldav library")
caldata.add_component(event)
caldata2 = Calendar()
caldata2.add("prodid", "-//tobixen//python-icalendar//en_DK")
caldata2.add("version", "2.0")
uid = uuid.uuid1()
event = Event()
event.add("dtstamp", datetime.now())
event.add("dtstart", datetime.now() + timedelta(days=4000))
event.add("dtend", datetime.now() + timedelta(days=4000, hours=1))
event.add("uid", uid)
event.add("summary", "Test event with participants but without invites")
caldata2.add_component(event)
## that event is without any attendee information. If saved to the
## calendar, it will only be stored locally, no invitations sent.
## There are two ways to send calendar invites:
## * Add Attendee-lines and an Organizer-line to the event data, and
## then use calendar.save_event(caldata) ... see RFC6638, appendix B.1
## for an example.
## * Use convenience-method calendar.save_with_invites(caldata, attendees).
## It will fetch organizer from the principal object. Method should
## accept different kind of attendees: strings, VCalAddress, (cn,
## email)-tuple and principal object.
## Lets make a list of attendees
attendees = []
## The organizer will invite himself. We'll pass a vCalAddress (from
## the icalendar library).
attendees.append(organizer.principal.get_vcal_address())
## Let's make it easy and add the other attendees by the Principal objects.
## note that we've used login credentials to get the principal
## objects below. One would normally need to know the principal
## URLs to create principal objects of other users, or perhaps use
## the principal-collection-set prop to get a list.
attendees.append(attendee1.principal)
attendees.append(attendee2.principal)
## An attendee can also be added by email address
attendees.append("some-random-guy@example.com")
## Or by a (common_name, email) tuple
attendees.append(("Some Other Random Guy", "some-other-random-guy@example.com"))
print("Sending a calendar invite")
organizer.calendar.save_with_invites(caldata, attendees=attendees)
print(
"Storing another calendar event with the same participants, but without sending out emails"
)
organizer.calendar.save_with_invites(
caldata2, attendees=attendees, schedule_agent="NONE"
)
## There are some attendee parameters that may be set (TODO: add
## example code), the convenience method above will use sensible
## defaults.
## The invite has now been shipped. The attendees should now respond to it.
print("looking into the inbox of attendee1")
all_cnt = 0
invite_req_cnt = 0
for inbox_item in attendee1.principal.schedule_inbox().get_items():
all_cnt += 1
## an inbox_item is an ordinary CalendarResourceObject/Event/Todo etc.
## is_invite_request will be implemented on the base class and will yield True
## for invite messages.
print("Inbox item found for attendee1. Here is the ical:")
print(inbox_item.data)
if inbox_item.is_invite_request():
print("Inbox item is an invite request")
invite_req_cnt += 1 ## TODO: assert(invite_req_cnt == 1) after loop
## Ref RFC6638, example B.3 ... to respond to an invite, it's
## needed to edit the ical data, find the correct
## "attendee"-field, change the attendee "partstat", put the
## ical object back to the server. In addition one has to
## look out for race conflicts and retry the whole operation
## in case of race conflicts. Editing ical data is a bit
## outside the scope of the CalDAV client library, but ... the
## library clearly needs convenience methods to deal with this.
## Invite objects will have methods accept_invite(),
## decline_invite(),
## tentatively_accept_invite(). .delete() is also an option
## (ref RFC6638, example B.2)
inbox_item.accept_invite()
inbox_item.delete()
## attendee2 has other long-term plans and can't join the event
for inbox_item in attendee2.principal.schedule_inbox().get_items():
print("found an inbox item for attendee 2, here is the ical:")
print(inbox_item.data)
if inbox_item.is_invite_request():
print("declining invite")
inbox_item.decline_invite()
inbox_item.delete()
## Oganizer will have an update on the participant status in the
## inbox (or perhaps two updates?) If I've understood the standard
## correctly, testuser0 should not get an invite and should not have
## to respond to it, but just in case we'll accept it. As far as I've
## understood, deleting the ical objects in the inbox should be
## harmless, it should still exist on the organizers calendar.
## (Example B.4 in RFC6638)
print("looking into organizers inbox")
for inbox_item in organizer.principal.schedule_inbox().get_items():
print("Inbox item found, here is the ical:")
print(inbox_item.data)
if inbox_item.is_invite_request():
print("It's an invite request, let's accept it")
inbox_item.accept_invite()
elif inbox_item.is_invite_reply():
print("It's an invite reply, now that we've read it, we can delete it")
inbox_item.delete()
## RFC6638/RFC5546 allows an organizer to check the freebusy status of
## multiple principals identified by email address. It's covered in
## section 4.3.2. in RFC5546 and chapter 5 / example B.5 in RFC6638.
## Most of the logic is on the icalendar format (covered in RFC5546),
## and is a bit outside the scope of the caldav client library.
## However, I will probably make a convenience method for doing the
## query, and leaving the parsing of the returned icalendar data to
## the user of the library:
import pdb
pdb.set_trace()
some_data_returned = organizer.principal.freebusy_request(
dtstart=datetime.now().astimezone(pytz.utc) + timedelta(days=399),
dtend=datetime.now().astimezone(pytz.utc) + timedelta(days=399, hours=1),
attendees=[attendee1.principal, attendee2.principal],
)
## Examples in RFC6638 goes on to describing how to accept and decline
## particular instances of a recurring events, and RFC5546 has a lot
## of extra information, like ways for a participant to signal back
## new suggestions for the meeting time, delegations, cancelling of
## events and whatnot. It is possible to use the library for such
## things by saving appropriate icalendar data to the outbox and
## reading things from the inbox, but as for now there aren't any
## planned convenience methods for covering such things.
|