async-caldav 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a2d260821de7e733cab42004a0f9a719fdbca8acf1071d193c63a3a8d10b90c8
4
+ data.tar.gz: 3218a7bd8fe1631232b2e4b4e07f279781ea643531dae9486db1ee872631c8bd
5
+ SHA512:
6
+ metadata.gz: d2c5cd6e1daaba511dd1932cc9c269317bc2e51cd9d539f144df1f5c8f8b698bfc329fdb5115f09a58ed5f3f1fb85cad2af2f5c99671429d767dc3ac2045d251
7
+ data.tar.gz: 0abf3e7374520f9ee561653157dd8f39f55bd23f6f54ba802aebb44deb9f21bce4638382a90d7470d3911d0f33b502247be18a1dcd41eb73ffbe01b0788a54ee
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+ require "scampi"
5
+
6
+ module Async
7
+ module Caldav
8
+ class Client
9
+ class Addressbook
10
+ attr_reader :path, :displayname
11
+
12
+ def initialize(client, path, props = {})
13
+ @client = client
14
+ @path = path
15
+ @displayname = props[:displayname]
16
+ end
17
+
18
+ def contacts(filter: nil)
19
+ body = if filter
20
+ <<~XML
21
+ <?xml version="1.0" encoding="UTF-8"?>
22
+ <cr:addressbook-query xmlns:d="DAV:" xmlns:cr="urn:ietf:params:xml:ns:carddav">
23
+ <d:prop><d:getetag/><cr:address-data/></d:prop>
24
+ #{filter}
25
+ </cr:addressbook-query>
26
+ XML
27
+ else
28
+ <<~XML
29
+ <?xml version="1.0" encoding="UTF-8"?>
30
+ <cr:addressbook-query xmlns:d="DAV:" xmlns:cr="urn:ietf:params:xml:ns:carddav">
31
+ <d:prop><d:getetag/><cr:address-data/></d:prop>
32
+ </cr:addressbook-query>
33
+ XML
34
+ end
35
+
36
+ status, _, resp_body = @client.request('REPORT', @path, body: body, headers: { 'Content-Type' => 'text/xml' })
37
+ raise Error, "REPORT failed: #{status}" unless status == 207
38
+
39
+ @client.parse_multistatus_items(resp_body, data_tag: 'address-data')
40
+ end
41
+
42
+ def put_contact(filename, body, if_match: nil, if_none_match: nil)
43
+ headers = { 'Content-Type' => 'text/vcard' }
44
+ headers['If-Match'] = if_match if if_match
45
+ headers['If-None-Match'] = if_none_match if if_none_match
46
+
47
+ path = "#{@path}#{filename}"
48
+ status, resp_headers, = @client.request('PUT', path, body: body, headers: headers)
49
+
50
+ case status
51
+ when 201, 204
52
+ { path: path, etag: resp_headers['etag'], status: status }
53
+ when 412
54
+ raise PreconditionFailed, "Precondition failed for #{path}"
55
+ when 409
56
+ raise Conflict, "UID conflict for #{path}"
57
+ else
58
+ raise Error, "PUT failed: #{status}"
59
+ end
60
+ end
61
+
62
+ def get_contact(filename)
63
+ path = "#{@path}#{filename}"
64
+ status, headers, body = @client.request('GET', path)
65
+
66
+ case status
67
+ when 200
68
+ { path: path, body: body, etag: headers['etag'], content_type: headers['content-type'] }
69
+ when 404
70
+ raise NotFound, "Not found: #{path}"
71
+ else
72
+ raise Error, "GET failed: #{status}"
73
+ end
74
+ end
75
+
76
+ def delete_contact(filename)
77
+ path = "#{@path}#{filename}"
78
+ status, = @client.request('DELETE', path)
79
+
80
+ case status
81
+ when 204 then true
82
+ when 404 then raise NotFound, "Not found: #{path}"
83
+ else raise Error, "DELETE failed: #{status}"
84
+ end
85
+ end
86
+
87
+ def delete
88
+ status, = @client.request('DELETE', @path)
89
+
90
+ case status
91
+ when 204 then true
92
+ when 404 then raise NotFound, "Not found: #{@path}"
93
+ else raise Error, "DELETE failed: #{status}"
94
+ end
95
+ end
96
+
97
+ def proppatch(displayname: nil)
98
+ props = []
99
+ props << "<d:displayname>#{Protocol::Caldav::Xml.escape(displayname)}</d:displayname>" if displayname
100
+
101
+ body = <<~XML
102
+ <?xml version="1.0" encoding="UTF-8"?>
103
+ <d:propertyupdate xmlns:d="DAV:">
104
+ <d:set><d:prop>#{props.join}</d:prop></d:set>
105
+ </d:propertyupdate>
106
+ XML
107
+
108
+ status, = @client.request('PROPPATCH', @path, body: body, headers: { 'Content-Type' => 'text/xml' })
109
+ raise Error, "PROPPATCH failed: #{status}" unless status == 207
110
+
111
+ @displayname = displayname if displayname
112
+ self
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+ require "scampi"
5
+
6
+ module Async
7
+ module Caldav
8
+ class Client
9
+ class Calendar
10
+ attr_reader :path, :displayname, :description, :color, :ctag
11
+
12
+ def initialize(client, path, props = {})
13
+ @client = client
14
+ @path = path
15
+ @displayname = props[:displayname]
16
+ @description = props[:description]
17
+ @color = props[:color]
18
+ @ctag = props[:ctag]
19
+ end
20
+
21
+ def events(filter: nil)
22
+ body = if filter
23
+ <<~XML
24
+ <?xml version="1.0" encoding="UTF-8"?>
25
+ <c:calendar-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
26
+ <d:prop><d:getetag/><c:calendar-data/></d:prop>
27
+ #{filter}
28
+ </c:calendar-query>
29
+ XML
30
+ else
31
+ <<~XML
32
+ <?xml version="1.0" encoding="UTF-8"?>
33
+ <c:calendar-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
34
+ <d:prop><d:getetag/><c:calendar-data/></d:prop>
35
+ </c:calendar-query>
36
+ XML
37
+ end
38
+
39
+ status, _, resp_body = @client.request('REPORT', @path, body: body, headers: { 'Content-Type' => 'text/xml' })
40
+ raise Error, "REPORT failed: #{status}" unless status == 207
41
+
42
+ @client.parse_multistatus_items(resp_body, data_tag: 'calendar-data')
43
+ end
44
+
45
+ def put_event(filename, body, if_match: nil, if_none_match: nil)
46
+ headers = { 'Content-Type' => 'text/calendar' }
47
+ headers['If-Match'] = if_match if if_match
48
+ headers['If-None-Match'] = if_none_match if if_none_match
49
+
50
+ path = "#{@path}#{filename}"
51
+ status, resp_headers, = @client.request('PUT', path, body: body, headers: headers)
52
+
53
+ case status
54
+ when 201, 204
55
+ { path: path, etag: resp_headers['etag'], status: status }
56
+ when 412
57
+ raise PreconditionFailed, "Precondition failed for #{path}"
58
+ when 409
59
+ raise Conflict, "UID conflict for #{path}"
60
+ else
61
+ raise Error, "PUT failed: #{status}"
62
+ end
63
+ end
64
+
65
+ def get_event(filename)
66
+ path = "#{@path}#{filename}"
67
+ status, headers, body = @client.request('GET', path)
68
+
69
+ case status
70
+ when 200
71
+ { path: path, body: body, etag: headers['etag'], content_type: headers['content-type'] }
72
+ when 404
73
+ raise NotFound, "Not found: #{path}"
74
+ else
75
+ raise Error, "GET failed: #{status}"
76
+ end
77
+ end
78
+
79
+ def delete_event(filename)
80
+ path = "#{@path}#{filename}"
81
+ status, = @client.request('DELETE', path)
82
+
83
+ case status
84
+ when 204 then true
85
+ when 404 then raise NotFound, "Not found: #{path}"
86
+ else raise Error, "DELETE failed: #{status}"
87
+ end
88
+ end
89
+
90
+ def delete
91
+ status, = @client.request('DELETE', @path)
92
+
93
+ case status
94
+ when 204 then true
95
+ when 404 then raise NotFound, "Not found: #{@path}"
96
+ else raise Error, "DELETE failed: #{status}"
97
+ end
98
+ end
99
+
100
+ def propfind
101
+ status, _, body = @client.request('PROPFIND', @path, headers: { 'Depth' => '0' })
102
+ raise Error, "PROPFIND failed: #{status}" unless status == 207
103
+ @client.parse_collection_props(body)
104
+ end
105
+
106
+ def proppatch(displayname: nil, description: nil, color: nil)
107
+ props = []
108
+ props << "<d:displayname>#{Protocol::Caldav::Xml.escape(displayname)}</d:displayname>" if displayname
109
+ props << "<c:calendar-description>#{Protocol::Caldav::Xml.escape(description)}</c:calendar-description>" if description
110
+ props << "<x:calendar-color>#{Protocol::Caldav::Xml.escape(color)}</x:calendar-color>" if color
111
+
112
+ body = <<~XML
113
+ <?xml version="1.0" encoding="UTF-8"?>
114
+ <d:propertyupdate xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav" xmlns:x="http://apple.com/ns/ical/">
115
+ <d:set><d:prop>#{props.join}</d:prop></d:set>
116
+ </d:propertyupdate>
117
+ XML
118
+
119
+ status, = @client.request('PROPPATCH', @path, body: body, headers: { 'Content-Type' => 'text/xml' })
120
+ raise Error, "PROPPATCH failed: #{status}" unless status == 207
121
+
122
+ @displayname = displayname if displayname
123
+ @description = description if description
124
+ @color = color if color
125
+ self
126
+ end
127
+
128
+ def sync(token: nil)
129
+ token_xml = token ? "<d:sync-token>#{token}</d:sync-token>" : "<d:sync-token/>"
130
+ body = <<~XML
131
+ <?xml version="1.0" encoding="UTF-8"?>
132
+ <d:sync-collection xmlns:d="DAV:">
133
+ <d:prop><d:getetag/></d:prop>
134
+ #{token_xml}
135
+ </d:sync-collection>
136
+ XML
137
+
138
+ status, _, resp_body = @client.request('REPORT', @path, body: body, headers: { 'Content-Type' => 'text/xml' })
139
+
140
+ if status == 403
141
+ raise InvalidSyncToken, "Invalid sync token"
142
+ end
143
+ raise Error, "REPORT failed: #{status}" unless status == 207
144
+
145
+ new_token = resp_body.match(/<[^>]*sync-token[^>]*>([^<]+)</)[1] rescue nil
146
+ items = @client.parse_sync_items(resp_body)
147
+ [items, new_token]
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end