calendav 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -2
- data/README.md +183 -39
- data/lib/calendav/calendar.rb +18 -42
- data/lib/calendav/client.rb +3 -5
- data/lib/calendav/clients/calendars_client.rb +60 -18
- data/lib/calendav/clients/events_client.rb +32 -13
- data/lib/calendav/contextual_url.rb +6 -3
- data/lib/calendav/endpoint.rb +43 -13
- data/lib/calendav/errors.rb +28 -0
- data/lib/calendav/event.rb +13 -35
- data/lib/calendav/multi_response.rb +27 -0
- data/lib/calendav/namespaces.rb +10 -0
- data/lib/calendav/parsers/calendar_xml.rb +58 -0
- data/lib/calendav/parsers/event_xml.rb +37 -0
- data/lib/calendav/parsers/response_xml.rb +48 -0
- data/lib/calendav/parsers/sync_xml.rb +57 -0
- data/lib/calendav/requests/calendar_home_set.rb +2 -2
- data/lib/calendav/requests/current_user_principal.rb +2 -2
- data/lib/calendav/requests/list_calendars.rb +32 -7
- data/lib/calendav/requests/list_events.rb +22 -10
- data/lib/calendav/requests/make_calendar.rb +8 -2
- data/lib/calendav/requests/sync_collection.rb +36 -0
- data/lib/calendav/requests/update_calendar.rb +63 -0
- data/lib/calendav/sync_collection.rb +13 -0
- data/spec/acceptance/apple_spec.rb +217 -0
- data/spec/acceptance/google_spec.rb +167 -26
- data/spec/spec_helper.rb +2 -0
- data/spec/support/encoded_matchers.rb +29 -0
- metadata +34 -7
- data/lib/calendav/xml_processor.rb +0 -42
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e72b5067dad00dc1074cbd5cf7d0b3c28845b624d47ff5e992e3b78a9a65fa0d
|
4
|
+
data.tar.gz: 11c9e3e362f36c91323141650a0d3415914f87aed208ea4716d8b0253ffd2c6e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 89da478d32baf4bbcfbd3c13ca2f89d5071f2dd4953889830d6f996d54bc4b5fb14feeac89af0c03891f047bf0e5b67fe7d88267df9ab0998a825387dbabf159
|
7
|
+
data.tar.gz: 05737755ec19e2c6b1e1f06b3f9273428e92d69381c19aee5cca14738bb9d63f243181a3c02b8d3d69ceb7e1b8c851847932aec8a81ae770a2d7c28d19357f12
|
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,20 @@
|
|
1
|
-
##
|
1
|
+
## Unreleased
|
2
2
|
|
3
|
-
|
3
|
+
...
|
4
|
+
|
5
|
+
## 0.1.0 - 2021-06-14
|
6
|
+
|
7
|
+
* Support creating and deleting calendars.
|
8
|
+
* Updating of events (with optional etag check).
|
9
|
+
* List all events on a calendar (no timespan required).
|
10
|
+
* Updating of calendars.
|
11
|
+
* Allow for etag check to be used on event deletions.
|
12
|
+
* Finding of single events and calendars by URLs.
|
13
|
+
* Check if calendar creation is possible.
|
14
|
+
* WebDAV-Sync support.
|
15
|
+
* A vastly more useful README.
|
16
|
+
|
17
|
+
## 0.0.1 - 2021-06-13
|
4
18
|
|
5
19
|
An initial release with very limited (and likely buggy) support:
|
6
20
|
|
data/README.md
CHANGED
@@ -2,75 +2,203 @@
|
|
2
2
|
|
3
3
|
A library for interacting with CalDAV servers via Ruby.
|
4
4
|
|
5
|
-
|
5
|
+
## Features
|
6
6
|
|
7
|
-
|
8
|
-
* Determining the calendar home path.
|
9
|
-
* Listing available calendars.
|
10
|
-
* Creating events on a calendar.
|
11
|
-
* Listing events on a calendar within a given timespan.
|
12
|
-
* Deleting events on a calendar.
|
7
|
+
### Credentials and Accounts
|
13
8
|
|
14
|
-
|
9
|
+
Calendav has support for a few calendar providers built-in by default: Apple, FastMail, and Google.
|
15
10
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
* Delete calendars.
|
22
|
-
* Enable etag validation for updates/deletions (If-Match header).
|
23
|
-
* Solid exception handling.
|
24
|
-
* Use WebDAV-Sync to get changes since last sync.
|
25
|
-
* Enable locking/unlocking when making changes.
|
26
|
-
* Allow requesting only certain properties for calendars/events.
|
11
|
+
```ruby
|
12
|
+
credentials = Calendav.credentials(
|
13
|
+
:apple, username: "example@icloud.com", password: "app-specific-password"
|
14
|
+
)
|
15
|
+
```
|
27
16
|
|
28
|
-
|
17
|
+
Both Apple and FastMail expect app-specific passwords (rather than the account's main password). Google expects an OAuth 2 access token for the password instead.
|
29
18
|
|
30
|
-
|
19
|
+
You can also create credentials for other providers:
|
31
20
|
|
32
21
|
```ruby
|
33
|
-
credentials = Calendav.credentials(
|
34
|
-
:google, username: "example@gmail.com", password: "oauth-access-token"
|
35
|
-
)
|
36
|
-
# Also supported are FastMail and Apple, where the passwords should be
|
37
|
-
# app-specific, with authentication via Basic Auth. Google uses a Bearer Token
|
38
|
-
# for authentication (supplied as the password).
|
39
|
-
#
|
40
|
-
# Otherwise, to compose a more custom set of credentials:
|
41
22
|
credentials = Calendav::Credentials::Standard.new(
|
42
23
|
host: "https://www.example.com/caldav",
|
43
24
|
username: "example",
|
44
25
|
password: "secret",
|
45
26
|
authentication: :basic_auth # or :bearer_token
|
46
27
|
)
|
28
|
+
```
|
47
29
|
|
48
|
-
client
|
30
|
+
You can use credentials to create a new client instance. If required, you can confirm they're valid by checking if a principal URL for the account can be returned.
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
client = Calendav.client(credentials)
|
49
34
|
puts client.principal_url
|
35
|
+
```
|
50
36
|
|
51
|
-
|
37
|
+
### Calendars
|
38
|
+
|
39
|
+
You can retrieve a list of all available calendars:
|
40
|
+
|
41
|
+
```ruby
|
52
42
|
calendars = client.calendars.list
|
53
|
-
calendars.each
|
43
|
+
calendars.each do |calendar|
|
44
|
+
puts calendar.url
|
45
|
+
puts calendar.display_name
|
46
|
+
end
|
47
|
+
```
|
48
|
+
|
49
|
+
All calendars returned will have a URL - and this is the primary and unique reference to the calendar, and will not change. They should also have a display name, description, etag, ctag, time zone and color - but not all providers support all of these properties.
|
50
|
+
|
51
|
+
If you already have the Calendar's URL, then you can request its information directly as well:
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
calendar = client.calendars.find(calendar_url)
|
55
|
+
```
|
56
|
+
|
57
|
+
Some providers (though not Google) allow you to create calendars via the CalDAV protocol. You must supply the identifier of this calendar, which is the final part of its URL - and so must be unique for the account. Using a UUID could be a good option.
|
58
|
+
|
59
|
+
```ruby
|
60
|
+
require "securerandom"
|
61
|
+
|
62
|
+
identifier = SecureRandom.uuid
|
63
|
+
|
64
|
+
calendar_url = client.calendars.create(identifier, display_name: "My Calendar")
|
65
|
+
```
|
54
66
|
|
67
|
+
You can also edit calendar details, and delete them (if the provide allows such actions). The allowed attributes are `display_name`, `description`, `time_zone` and `color`.
|
68
|
+
|
69
|
+
```ruby
|
70
|
+
client.calendars.update(calendar_url, display_name: "Altered Calendar")
|
71
|
+
client.calendars.delete(calendar_url)
|
72
|
+
```
|
73
|
+
|
74
|
+
It is possible to check whether a calendar provider supports calendar creation/deletion:
|
75
|
+
|
76
|
+
```ruby
|
77
|
+
client.calendars.create? # => true/false
|
78
|
+
```
|
79
|
+
|
80
|
+
Also, if it's useful, you can access the account's calendar home path (the root directory for that account's calendars):
|
81
|
+
|
82
|
+
```ruby
|
83
|
+
client.calendars.home_url
|
84
|
+
```
|
85
|
+
|
86
|
+
### Events
|
87
|
+
|
88
|
+
Once you have a calendar's URL, you can retrieve events from it. Unless you are making a copy of the calendar for synchronisation purposes, it's recommended you limit the request to a specific timeframe (though the `from` and `to` arguments are optional).
|
89
|
+
|
90
|
+
```ruby
|
55
91
|
events = client.events.list(
|
56
|
-
|
92
|
+
calendar_url, from: Time.new(2021, 1, 1), to: Time.new(2022, 1, 1)
|
57
93
|
)
|
94
|
+
events.each do |event|
|
95
|
+
puts event.url
|
96
|
+
puts event.summary
|
97
|
+
puts event.calendar_data
|
98
|
+
end
|
99
|
+
```
|
100
|
+
|
101
|
+
The returned events have a unique URL (just like calendars), an `etag` (which changes when the event changes), and `calendar_data`, which is stored in the [iCalendar](https://datatracker.ietf.org/doc/html/rfc5545) format. The event objects returned currently parse the summary out of the calendar data, but nothing else - further attributes may be made visible, but for full control it's recommended you use the [icalendar](https://github.com/icalendar/icalendar) gem.
|
102
|
+
|
103
|
+
If you have an event's URL, you can fetch the details of just that event directly:
|
104
|
+
|
105
|
+
```ruby
|
106
|
+
event = client.events.find(event_url)
|
107
|
+
```
|
108
|
+
|
109
|
+
Creating events, just like creating calendars, requires a unique identifier. There are no hard requirements for the format from the perspective of CalDAV generally, but some providers require the identifier to have the extension `.ics`.
|
110
|
+
|
111
|
+
You will also need to generate the iCalendar data - again, the [icalendar](https://github.com/icalendar/icalendar) gem is very helpful for this.
|
112
|
+
|
113
|
+
```ruby
|
114
|
+
require "securerandom"
|
115
|
+
require "icalendar"
|
58
116
|
|
59
|
-
# use the icalendar gem to generate ICS strings
|
60
117
|
ics = Icalendar::Calendar.new
|
61
118
|
ics.event do |event|
|
62
119
|
# ...
|
63
120
|
end
|
64
121
|
ics.publish
|
65
122
|
|
66
|
-
# You need to provide the expected filename for the event:
|
67
123
|
identifier = "#{SecureRandom.uuid}.ics"
|
68
|
-
|
69
|
-
|
124
|
+
event_url = client.events.create(calendar.url, identifier, ics.to_ical)
|
125
|
+
```
|
126
|
+
|
127
|
+
*Please note*: some providers (definitely Google, possibly others) will not keep your supplied event identifier, but generate a new one. So, if you need an ongoing reference to the event, save the returned URL (rather than combining the calendar URL and your identifier).
|
128
|
+
|
129
|
+
Updating events is done in a similar manner - with the event's URL and the updated iCalendar content:
|
130
|
+
|
131
|
+
```ruby
|
132
|
+
client.events.update(event_url, ics.to_ical)
|
133
|
+
```
|
134
|
+
|
135
|
+
To ensure you're only changing a known version of an event, it's recommended you use that version's etag as a precondition check (the update call will return false if the event on the server has a different etag value):
|
136
|
+
|
137
|
+
```ruby
|
138
|
+
event = client.events.find(event_url)
|
139
|
+
|
140
|
+
# figure out the changes you want to make, generate the new ical data, and then:
|
141
|
+
|
142
|
+
client.events.update(event_url, modified_ical, etag: event.etag)
|
143
|
+
```
|
70
144
|
|
145
|
+
Deletion of events is done via the event's URL as well - some providers (including Apple) support the etag precondition check for this, but others (including Google) do not. In the latter situation, the deletion will happen regardless of the server event's etag value.
|
146
|
+
|
147
|
+
```ruby
|
71
148
|
client.events.delete(event_url)
|
149
|
+
|
150
|
+
client.events.delete(event_url, etag: event.etag)
|
151
|
+
```
|
152
|
+
|
153
|
+
### Synchronising
|
154
|
+
|
155
|
+
If you are maintaining your own copy/history of events from a CalDAV server, then it's highly recommended you take advantage of the WebDAV-Sync protocol (should the calendar provider support it). Using Calendav, the suggested approach is:
|
156
|
+
|
157
|
+
* Get a token for a specific calendar
|
158
|
+
* Request all events for that calendar
|
159
|
+
* Then, to get just the changed/deleted events, use the token to request the delta.
|
160
|
+
* That request will return a new token, which you use in the _next_ delta request, and so forth.
|
161
|
+
|
162
|
+
These `sync_token` values are not returned by default, but can be requested on a per-calendar basis:
|
163
|
+
|
164
|
+
```ruby
|
165
|
+
token = subject.calendars.find(calendar_url, sync: true).sync_token
|
166
|
+
```
|
167
|
+
|
168
|
+
To retrieve all events for that calendar (when starting the initial synchronisation process):
|
169
|
+
|
170
|
+
```ruby
|
171
|
+
events = subject.events.list(calendar_url)
|
72
172
|
```
|
73
173
|
|
174
|
+
And then to retrieve the delta changes and a new `sync_token`:
|
175
|
+
|
176
|
+
```ruby
|
177
|
+
collection = subject.calendars.sync(calendar_url, token)
|
178
|
+
collection.changes.each { |change| puts change.url }
|
179
|
+
collection.deletions.each { |deletion_url| puts deletion_url }
|
180
|
+
token = collection.sync_token
|
181
|
+
```
|
182
|
+
|
183
|
+
The `deletions` array is the event URLs for any events that have been removed since your previous sync - no other details are available.
|
184
|
+
|
185
|
+
The `changes` array are Event objects - but some may not have their `calendar_data` populated, as not all calendar providers supply this as part of requesting the synchronisation delta. Apple does not return this information, but Google does. So, you may need to request the full event object if required:
|
186
|
+
|
187
|
+
```ruby
|
188
|
+
events = collection.changes.collect do |event|
|
189
|
+
event.unloaded? ? client.events.find(event.url) : event
|
190
|
+
end
|
191
|
+
```
|
192
|
+
|
193
|
+
### Still to be implemented
|
194
|
+
|
195
|
+
While a lot of the core CalDAV functionality is covered and this gem is useful as it stands, the following features are on the roadmap:
|
196
|
+
|
197
|
+
* Further iCalendar parsing for Event objects.
|
198
|
+
* Automated tests against FastMail and possibly other providers (Apple and Google are already covered).
|
199
|
+
* Locking/unlocking of events as per the WebCAL RFC.
|
200
|
+
* Support for VTODO, VJOURNAL, VFREEBUSY and any other components beyond VEVENT.
|
201
|
+
|
74
202
|
## Installation
|
75
203
|
|
76
204
|
Add this line to your application's Gemfile:
|
@@ -87,16 +215,32 @@ Or install it yourself as:
|
|
87
215
|
|
88
216
|
$ gem install calendav
|
89
217
|
|
218
|
+
## API Design
|
219
|
+
|
220
|
+
I've thus far preferred separation between data objects (`Calendav::Event` and `Calendav::Calendar`), and the interaction layer (via `Calendav::Client`) - as opposed to, say, a more ActiveRecord-like manner of interactions from within data objects.
|
221
|
+
|
222
|
+
I've chosen the separated approach because it allows actions to occur on specific calendars or events without having a full data tree - this keeps requests to calendar providers to a minimum. You don't want to be loading everything from their servers every time you're reading/modifying one event!
|
223
|
+
|
224
|
+
One thing that will likely evolve is how identifiers are handled for new calendars and events - while allowing custom ones to be provided will remain, I'll be looking at autogeneration with UUIDs to ensure a simpler approach is possible.
|
225
|
+
|
226
|
+
Similarly, providing translation around event/calendar/time-zone details (via [icalendar](https://github.com/icalendar/icalendar)) is also a consideration.
|
227
|
+
|
228
|
+
## Thanks
|
229
|
+
|
230
|
+
The work done in previous Ruby CalDAV clients [RubyCaldav](https://github.com/digITpro/caldav_client) and [caldav](https://github.com/collectiveidea/caldav) has been very helpful, even though the codebases haven't seen updates in many years. I also found Sabre's documentation on [building a CalDAV client](https://sabre.io/dav/building-a-caldav-client/) to be extremely useful. Thank you to those teams for their hard work!
|
231
|
+
|
90
232
|
## Development
|
91
233
|
|
92
234
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
93
235
|
|
236
|
+
## Tests
|
237
|
+
|
238
|
+
The test suite currently only runs against Google and Apple accounts I've created especially for this purpose - and the credentials are not public. I realise this makes contributions more difficult, and I'm open to finding better ways to handle this. I did look into running a CalDAV server within the test suite, but couldn't find anything small and easy enough for that purpose. But also: testing against common CalDAV servers does help to ensure this gem is truly useful (and has knowledge of their idiosyncrasies).
|
239
|
+
|
94
240
|
## Contributing
|
95
241
|
|
96
242
|
Bug reports and pull requests are welcome on GitHub at https://github.com/pat/calendav. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/pat/calendav/blob/main/CODE_OF_CONDUCT.md).
|
97
243
|
|
98
|
-
The test suite currently only runs against a Google account I've created especially for this purpose - and the credentials are not public. I realise this makes contributions more difficult, and I'm open to finding better ways to handle this. I did look into running a CalDAV server within the test suite, but couldn't find anything small and easy enough for that purpose. But also: testing against common CalDAV servers does help to ensure this gem is truly useful (and has knowledge of their idiosyncrasies).
|
99
|
-
|
100
244
|
## License
|
101
245
|
|
102
246
|
The gem is available as open source under the terms of the [Hippocratic License](https://firstdonoharm.dev).
|
data/lib/calendav/calendar.rb
CHANGED
@@ -1,55 +1,31 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "./
|
3
|
+
require_relative "./contextual_url"
|
4
|
+
require_relative "./parsers/calendar_xml"
|
4
5
|
|
5
6
|
module Calendav
|
6
7
|
class Calendar
|
7
|
-
attr_reader :
|
8
|
+
attr_reader :url, :display_name, :description, :ctag, :etag, :time_zone,
|
9
|
+
:color, :components, :reports, :sync_token
|
8
10
|
|
9
|
-
def self.from_xml(node)
|
11
|
+
def self.from_xml(host, node)
|
10
12
|
new(
|
11
|
-
node.xpath("./dav:href").text,
|
12
|
-
|
13
|
-
node.namespaces
|
13
|
+
ContextualURL.call(host, node.xpath("./dav:href").text),
|
14
|
+
Parsers::CalendarXML.call(node)
|
14
15
|
)
|
15
16
|
end
|
16
17
|
|
17
|
-
def initialize(
|
18
|
-
@
|
19
|
-
@
|
20
|
-
@
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
attribute_value fragment.xpath("//cs:getctag")
|
29
|
-
end
|
30
|
-
|
31
|
-
def etag
|
32
|
-
attribute_value fragment.xpath("//dav:getetag")
|
33
|
-
end
|
34
|
-
|
35
|
-
private
|
36
|
-
|
37
|
-
attr_reader :attribute_nodes, :namespaces
|
38
|
-
|
39
|
-
def fragment
|
40
|
-
@fragment ||= XMLProcessor.call(
|
41
|
-
"<nodes>#{attribute_nodes}</nodes>", namespaces
|
42
|
-
)
|
43
|
-
end
|
44
|
-
|
45
|
-
def attribute_value(node)
|
46
|
-
return nil if node.children.empty?
|
47
|
-
|
48
|
-
if node.children.any?(&:element?)
|
49
|
-
node.children.select(&:element?).collect(&:to_xml).join
|
50
|
-
else
|
51
|
-
node.children.text
|
52
|
-
end
|
18
|
+
def initialize(url, attributes = {})
|
19
|
+
@url = url
|
20
|
+
@display_name = attributes[:display_name]
|
21
|
+
@description = attributes[:description]
|
22
|
+
@ctag = attributes[:ctag]
|
23
|
+
@etag = attributes[:etag]
|
24
|
+
@time_zone = attributes[:time_zone]
|
25
|
+
@color = attributes[:color]
|
26
|
+
@components = attributes[:components]
|
27
|
+
@reports = attributes[:reports]
|
28
|
+
@sync_token = attributes[:sync_token]
|
53
29
|
end
|
54
30
|
end
|
55
31
|
end
|
data/lib/calendav/client.rb
CHANGED
@@ -23,13 +23,11 @@ module Calendav
|
|
23
23
|
def principal_url
|
24
24
|
@principal_url ||= begin
|
25
25
|
request = Requests::CurrentUserPrincipal.call
|
26
|
+
response = endpoint.propfind(request.to_xml).first
|
26
27
|
|
27
28
|
ContextualURL.call(
|
28
|
-
credentials,
|
29
|
-
|
30
|
-
.propfind(request.to_xml)
|
31
|
-
.xpath(".//dav:current-user-principal/dav:href")
|
32
|
-
.text
|
29
|
+
credentials.host,
|
30
|
+
response.xpath(".//dav:current-user-principal/dav:href").text
|
33
31
|
)
|
34
32
|
end
|
35
33
|
end
|
@@ -3,13 +3,20 @@
|
|
3
3
|
require "securerandom"
|
4
4
|
|
5
5
|
require_relative "../calendar"
|
6
|
+
require_relative "../parsers/sync_xml"
|
6
7
|
require_relative "../requests/calendar_home_set"
|
7
8
|
require_relative "../requests/list_calendars"
|
8
9
|
require_relative "../requests/make_calendar"
|
10
|
+
require_relative "../requests/sync_collection"
|
11
|
+
require_relative "../requests/update_calendar"
|
9
12
|
|
10
13
|
module Calendav
|
11
14
|
module Clients
|
12
15
|
class CalendarsClient
|
16
|
+
DEFAULT_ATTRIBUTES = %i[
|
17
|
+
display_name resource_type etag ctag color components reports
|
18
|
+
].freeze
|
19
|
+
|
13
20
|
def initialize(client, endpoint, credentials)
|
14
21
|
@client = client
|
15
22
|
@endpoint = endpoint
|
@@ -19,47 +26,82 @@ module Calendav
|
|
19
26
|
def home_url
|
20
27
|
@home_url ||= begin
|
21
28
|
request = Requests::CalendarHomeSet.call
|
29
|
+
response = endpoint.propfind(request.to_xml, url: principal_url).first
|
22
30
|
|
23
31
|
ContextualURL.call(
|
24
|
-
credentials,
|
25
|
-
|
26
|
-
.propfind(request.to_xml, url: client.principal_url)
|
27
|
-
.xpath(".//caldav:calendar-home-set/dav:href")
|
28
|
-
.text
|
32
|
+
credentials.host,
|
33
|
+
response.xpath(".//caldav:calendar-home-set/dav:href").text
|
29
34
|
)
|
30
35
|
end
|
31
36
|
end
|
32
37
|
|
33
|
-
def create
|
34
|
-
|
35
|
-
|
36
|
-
id = SecureRandom.uuid
|
37
|
-
id = "/#{id}" unless home_url.end_with?("/")
|
38
|
-
url = home_url + id
|
38
|
+
def create?
|
39
|
+
options.include?("MKCOL") || options.include?("MKCALENDAR")
|
40
|
+
end
|
39
41
|
|
40
|
-
|
42
|
+
def create(identifier, attributes)
|
43
|
+
request = Requests::MakeCalendar.call(attributes)
|
44
|
+
url = merged_url(identifier)
|
45
|
+
result = endpoint.mkcalendar(request.to_xml, url: url)
|
41
46
|
|
42
|
-
url
|
47
|
+
result.headers["Location"] || url
|
43
48
|
end
|
44
49
|
|
45
50
|
def delete(url)
|
46
51
|
endpoint.delete(url: url)
|
47
52
|
end
|
48
53
|
|
49
|
-
def
|
50
|
-
|
54
|
+
def find(url, attributes: DEFAULT_ATTRIBUTES, sync: false)
|
55
|
+
attributes = (attributes.dup << :sync_token) if sync
|
56
|
+
|
57
|
+
list(url, depth: 0, attributes: attributes).first
|
58
|
+
end
|
59
|
+
|
60
|
+
def list(url = home_url, depth: 1, attributes: DEFAULT_ATTRIBUTES)
|
61
|
+
request = Requests::ListCalendars.call(attributes)
|
51
62
|
calendar_xpath = ".//dav:resourcetype/caldav:calendar"
|
52
63
|
|
53
64
|
endpoint
|
54
|
-
.propfind(request.to_xml, url:
|
55
|
-
.xpath(".//dav:response")
|
65
|
+
.propfind(request.to_xml, url: url, depth: depth)
|
56
66
|
.select { |node| node.xpath(calendar_xpath).any? }
|
57
|
-
.collect { |node| Calendar.from_xml(node) }
|
67
|
+
.collect { |node| Calendar.from_xml(home_url, node) }
|
68
|
+
end
|
69
|
+
|
70
|
+
def options
|
71
|
+
endpoint
|
72
|
+
.options(url: home_url)
|
73
|
+
.headers["Allow"]
|
74
|
+
.split(", ")
|
75
|
+
end
|
76
|
+
|
77
|
+
def sync(url, token)
|
78
|
+
request = Requests::SyncCollection.call(token)
|
79
|
+
|
80
|
+
Parsers::SyncXML.call(
|
81
|
+
url, endpoint.report(request.to_xml, url: url, depth: nil)
|
82
|
+
)
|
83
|
+
end
|
84
|
+
|
85
|
+
def update(url, attributes)
|
86
|
+
request = Requests::UpdateCalendar.call(attributes)
|
87
|
+
endpoint
|
88
|
+
.proppatch(request.to_xml, url: url)
|
89
|
+
.first
|
90
|
+
.xpath(".//dav:status")
|
91
|
+
.text["200 OK"] == "200 OK"
|
58
92
|
end
|
59
93
|
|
60
94
|
private
|
61
95
|
|
62
96
|
attr_reader :client, :endpoint, :credentials
|
97
|
+
|
98
|
+
def merged_url(identifier)
|
99
|
+
"#{home_url.delete_suffix('/')}/#{identifier.delete_suffix('/')}/"
|
100
|
+
end
|
101
|
+
|
102
|
+
def principal_url
|
103
|
+
client.principal_url
|
104
|
+
end
|
63
105
|
end
|
64
106
|
end
|
65
107
|
end
|