google_r 0.2.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 +7 -0
- data/.github/workflows/ci.yml +26 -0
- data/.github/workflows/gempush.yml +28 -0
- data/.gitignore +7 -0
- data/.rspec +3 -0
- data/CHANGELOG.md +12 -0
- data/Gemfile +4 -0
- data/LICENSE +201 -0
- data/README.md +18 -0
- data/Rakefile +1 -0
- data/google_r.gemspec +27 -0
- data/lib/google_r.rb +201 -0
- data/lib/google_r/calendar.rb +58 -0
- data/lib/google_r/contact.rb +241 -0
- data/lib/google_r/event.rb +99 -0
- data/lib/google_r/exceptions.rb +16 -0
- data/lib/google_r/group.rb +88 -0
- data/lib/google_r/token.rb +44 -0
- data/lib/google_r/version.rb +3 -0
- data/spec/fixtures/contact_list.xml +98 -0
- data/spec/fixtures/no_contacts.xml +20 -0
- data/spec/fixtures/single_contact.xml +75 -0
- data/spec/fixtures/single_group.xml +14 -0
- data/spec/google_api_spec.rb +90 -0
- data/spec/google_contact_spec.rb +428 -0
- data/spec/google_event_spec.rb +33 -0
- data/spec/google_group_spec.rb +46 -0
- data/spec/spec_helper.rb +7 -0
- metadata +156 -0
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
|
3
|
+
class GoogleR::Calendar
|
4
|
+
attr_accessor :google_id, :etag, :summary, :description, :time_zone
|
5
|
+
|
6
|
+
def self.url
|
7
|
+
"https://www.googleapis.com"
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.api_headers
|
11
|
+
{
|
12
|
+
'GData-Version' => '3.0',
|
13
|
+
'Content-Type' => 'application/json',
|
14
|
+
}
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.path
|
18
|
+
"/calendar/v3/users/me/calendarList"
|
19
|
+
end
|
20
|
+
|
21
|
+
def path
|
22
|
+
if new?
|
23
|
+
"/calendar/v3/calendars"
|
24
|
+
else
|
25
|
+
"/calendar/v3/calendars/#{google_id}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.from_json(json, *attrs)
|
30
|
+
if json["kind"] == "calendar#calendar" || json["kind"] == "calendar#calendarListEntry"
|
31
|
+
calendar = self.new
|
32
|
+
calendar.google_id = json["id"]
|
33
|
+
calendar.etag = json["etag"]
|
34
|
+
calendar.summary = json["summary"]
|
35
|
+
calendar.description = json["description"]
|
36
|
+
calendar.time_zone = json["timeZone"]
|
37
|
+
calendar
|
38
|
+
else
|
39
|
+
raise "Not implemented:\n#{json.inspect}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def to_google(yajl_opts = {})
|
44
|
+
hash = {
|
45
|
+
"kind" => "calendar#calendar",
|
46
|
+
}
|
47
|
+
hash["etag"] = etag if etag
|
48
|
+
hash["id"] = google_id if google_id
|
49
|
+
hash["summary"] = summary if summary
|
50
|
+
hash["description"] = description if description
|
51
|
+
hash["timeZone"] = time_zone if time_zone
|
52
|
+
Yajl::Encoder.encode(hash, yajl_opts)
|
53
|
+
end
|
54
|
+
|
55
|
+
def new?
|
56
|
+
self.google_id.nil?
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,241 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
|
3
|
+
class GoogleR::Contact
|
4
|
+
Email = Struct.new(:address, :display_name, :label, :rel, :primary)
|
5
|
+
Phone = Struct.new(:rel, :text)
|
6
|
+
Organization = Struct.new(:name, :title, :rel)
|
7
|
+
Address = Struct.new(:street, :neighborhood, :pobox, :postcode, :city, :region, :country, :rel)
|
8
|
+
Website = Struct.new(:href, :rel)
|
9
|
+
|
10
|
+
attr_reader :emails, :phones, :organizations, :addresses, :groups, :websites
|
11
|
+
attr_accessor :given_name, :additional_name, :name_prefix, :name_suffix, :family_name,
|
12
|
+
:google_id, :etag, :content, :updated, :user_fields, :nickname
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
@emails = []
|
16
|
+
@phones = []
|
17
|
+
@organizations = []
|
18
|
+
@addresses = []
|
19
|
+
@groups = []
|
20
|
+
@websites = []
|
21
|
+
@user_fields = {}
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.url
|
25
|
+
"https://www.google.com"
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.path
|
29
|
+
"/m8/feeds/contacts/default/full/"
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.api_headers
|
33
|
+
{
|
34
|
+
'GData-Version' => '3.0',
|
35
|
+
'Content-Type' => 'application/atom+xml',
|
36
|
+
}
|
37
|
+
end
|
38
|
+
|
39
|
+
def path
|
40
|
+
if new?
|
41
|
+
self.class.path
|
42
|
+
else
|
43
|
+
self.class.path + google_id.split("/")[-1]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def full_name
|
48
|
+
[name_prefix, given_name, additional_name, family_name, name_suffix].compact.join(" ")
|
49
|
+
end
|
50
|
+
|
51
|
+
def to_google
|
52
|
+
builder = Nokogiri::XML::Builder.new(:encoding => "UTF-8") do |xml|
|
53
|
+
root_attrs = {
|
54
|
+
'xmlns:atom' => 'http://www.w3.org/2005/Atom',
|
55
|
+
'xmlns:gd' => 'http://schemas.google.com/g/2005',
|
56
|
+
'xmlns:gContact' => 'http://schemas.google.com/contact/2008',
|
57
|
+
}
|
58
|
+
root_attrs["gd:etag"] = self.etag unless new?
|
59
|
+
xml.entry(root_attrs) do
|
60
|
+
unless new?
|
61
|
+
xml.id_ self.google_id
|
62
|
+
end
|
63
|
+
|
64
|
+
xml.updated self.updated.strftime("%Y-%m-%dT%H:%M:%S.%LZ") unless self.updated.nil?
|
65
|
+
|
66
|
+
if self.full_name != ''
|
67
|
+
xml['gd'].name do
|
68
|
+
xml['gd'].givenName self.given_name unless self.given_name.nil?
|
69
|
+
xml['gd'].additionalName self.additional_name unless self.additional_name.nil?
|
70
|
+
xml['gd'].familyName self.family_name unless self.family_name.nil?
|
71
|
+
xml['gd'].namePrefix self.name_prefix unless self.name_prefix.nil?
|
72
|
+
xml['gd'].nameSuffix self.name_suffix unless self.name_suffix.nil?
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
xml['atom'].content({'type' => 'text'}, self.content) unless self.content.nil?
|
77
|
+
xml['gContact'].nickname self.nickname unless self.nickname.nil?
|
78
|
+
|
79
|
+
phones.each do |phone|
|
80
|
+
xml['gd'].phoneNumber({'rel' => phone.rel}, phone.text)
|
81
|
+
end
|
82
|
+
|
83
|
+
emails.each do |email|
|
84
|
+
attrs = {'address' => email.address}
|
85
|
+
attrs['rel'] = email.rel if email.rel
|
86
|
+
attrs['label'] = email.label if email.label
|
87
|
+
attrs['primary'] = email.primary
|
88
|
+
xml['gd'].email(attrs)
|
89
|
+
end
|
90
|
+
|
91
|
+
organizations.each do |org|
|
92
|
+
xml['gd'].organization({:rel => org.rel}) do
|
93
|
+
xml['gd'].orgName org.name
|
94
|
+
xml['gd'].orgTitle org.title
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
addresses.each do |address|
|
99
|
+
xml['gd'].structuredPostalAddress({'rel' => address.rel}) do
|
100
|
+
xml['gd'].street address.street unless address.street.nil?
|
101
|
+
xml['gd'].neighborhood address.neighborhood unless address.neighborhood.nil?
|
102
|
+
xml['gd'].pobox address.pobox unless address.pobox.nil?
|
103
|
+
xml['gd'].postcode address.postcode unless address.postcode.nil?
|
104
|
+
xml['gd'].city address.city unless address.city.nil?
|
105
|
+
xml['gd'].region address.region unless address.region.nil?
|
106
|
+
xml['gd'].country address.country unless address.country.nil?
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
user_fields.each do |key, value|
|
111
|
+
xml['gContact'].userDefinedField({'key' => key, 'value' => value})
|
112
|
+
end
|
113
|
+
|
114
|
+
websites.each do |website|
|
115
|
+
xml['gContact'].website({'href' => website.href, 'rel' => website.rel})
|
116
|
+
end
|
117
|
+
|
118
|
+
groups.each do |group|
|
119
|
+
xml['gContact'].groupMembershipInfo({'href' => group.google_id, 'deleted' => 'false'})
|
120
|
+
end
|
121
|
+
|
122
|
+
xml.parent.namespace = xml.parent.namespace_definitions.find { |ns| ns.prefix == "atom" }
|
123
|
+
end
|
124
|
+
end
|
125
|
+
builder.to_xml
|
126
|
+
end
|
127
|
+
|
128
|
+
def add_email(email)
|
129
|
+
@emails << email
|
130
|
+
end
|
131
|
+
|
132
|
+
def add_phone(phone)
|
133
|
+
@phones << phone
|
134
|
+
end
|
135
|
+
|
136
|
+
def add_organization(organization)
|
137
|
+
@organizations << organization
|
138
|
+
end
|
139
|
+
|
140
|
+
def add_address(address)
|
141
|
+
@addresses << address
|
142
|
+
end
|
143
|
+
|
144
|
+
def add_group(group)
|
145
|
+
@groups << group
|
146
|
+
end
|
147
|
+
|
148
|
+
def add_website(website)
|
149
|
+
@websites << website
|
150
|
+
end
|
151
|
+
|
152
|
+
def self.from_xml(doc, *attrs)
|
153
|
+
is_collection = doc.search("totalResults").size > 0
|
154
|
+
return doc.search("entry").map { |e| from_xml(e) } if is_collection
|
155
|
+
|
156
|
+
contact = GoogleR::Contact.new
|
157
|
+
|
158
|
+
google_id = doc.search("id")
|
159
|
+
|
160
|
+
if google_id.empty?
|
161
|
+
contact.etag = contact.google_id = nil
|
162
|
+
else
|
163
|
+
contact.etag = doc["etag"]
|
164
|
+
contact.google_id = google_id.inner_text
|
165
|
+
end
|
166
|
+
|
167
|
+
doc.search("email").each do |email|
|
168
|
+
contact.add_email(GoogleR::Contact::Email.new(email[:address], email[:display_name], email[:label], email[:rel], email[:primary] == "true"))
|
169
|
+
end
|
170
|
+
|
171
|
+
doc.search("phoneNumber").each do |phone|
|
172
|
+
contact.add_phone(GoogleR::Contact::Phone.new(phone[:rel], phone.inner_text))
|
173
|
+
end
|
174
|
+
|
175
|
+
doc.search("organization").each do |org|
|
176
|
+
name = org.search("orgName").inner_text
|
177
|
+
title = org.search("orgTitle").inner_text
|
178
|
+
rel = org[:rel]
|
179
|
+
contact.add_organization(GoogleR::Contact::Organization.new(name, title, rel))
|
180
|
+
end
|
181
|
+
|
182
|
+
doc.search("structuredPostalAddress").each do |address|
|
183
|
+
rel = address[:rel]
|
184
|
+
street = address.search("street").inner_text
|
185
|
+
neighborhood = address.search("neighborhood").inner_text
|
186
|
+
pobox = address.search("pobox").inner_text
|
187
|
+
postcode = address.search("postcode").inner_text
|
188
|
+
city = address.search("city").inner_text
|
189
|
+
region = address.search("region").inner_text
|
190
|
+
country = address.search("country").inner_text
|
191
|
+
contact.add_address(GoogleR::Contact::Address.new(street, neighborhood, pobox, postcode, city, region, country, rel))
|
192
|
+
end
|
193
|
+
|
194
|
+
doc.search("userDefinedField").each do |field|
|
195
|
+
contact.user_fields[field[:key]] = field[:value]
|
196
|
+
end
|
197
|
+
|
198
|
+
doc.search("groupMembershipInfo").each do |entry|
|
199
|
+
group = GoogleR::Group.new
|
200
|
+
group.google_id = entry[:href]
|
201
|
+
contact.add_group(group)
|
202
|
+
end
|
203
|
+
|
204
|
+
doc.search("website").each do |entry|
|
205
|
+
website = GoogleR::Contact::Website.new
|
206
|
+
website.href = entry[:href]
|
207
|
+
website.rel = entry[:rel]
|
208
|
+
contact.add_website(website)
|
209
|
+
end
|
210
|
+
|
211
|
+
name_prefix = doc.search("name/namePrefix")
|
212
|
+
contact.name_prefix = name_prefix.inner_text unless name_prefix.empty?
|
213
|
+
|
214
|
+
given_name = doc.search("name/givenName")
|
215
|
+
contact.given_name = given_name.inner_text unless given_name.empty?
|
216
|
+
|
217
|
+
additional_name = doc.search("name/additionalName")
|
218
|
+
contact.additional_name = additional_name.inner_text unless additional_name.empty?
|
219
|
+
|
220
|
+
family_name = doc.search("name/familyName")
|
221
|
+
contact.family_name = family_name.inner_text unless family_name.empty?
|
222
|
+
|
223
|
+
name_suffix = doc.search("name/nameSuffix")
|
224
|
+
contact.name_suffix = name_suffix.inner_text unless name_suffix.empty?
|
225
|
+
|
226
|
+
content = doc.search("content")
|
227
|
+
contact.content = content.inner_text unless content.empty?
|
228
|
+
|
229
|
+
updated = doc.search("updated")
|
230
|
+
contact.updated = Time.parse(updated.inner_text) unless updated.empty?
|
231
|
+
|
232
|
+
nickname = doc.search("nickname")
|
233
|
+
contact.nickname = nickname.inner_text unless nickname.empty?
|
234
|
+
|
235
|
+
contact
|
236
|
+
end
|
237
|
+
|
238
|
+
def new?
|
239
|
+
self.google_id.nil?
|
240
|
+
end
|
241
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
|
3
|
+
class GoogleR::Event
|
4
|
+
attr_accessor :google_id, :etag, :start_time, :end_time, :calendar, :description, :summary,
|
5
|
+
:visibility, :updated, :created, :status, :start_time_zone, :end_time_zone
|
6
|
+
|
7
|
+
def initialize(calendar, opts = {})
|
8
|
+
self.calendar = calendar
|
9
|
+
self.visibility = opts[:visibility] || "private"
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.url
|
13
|
+
"https://www.googleapis.com"
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.api_headers
|
17
|
+
{
|
18
|
+
'GData-Version' => '3.0',
|
19
|
+
'Content-Type' => 'application/json',
|
20
|
+
}
|
21
|
+
end
|
22
|
+
|
23
|
+
def path
|
24
|
+
if new?
|
25
|
+
"/calendar/v3/calendars/#{calendar.google_id}/events"
|
26
|
+
else
|
27
|
+
"/calendar/v3/calendars/#{calendar.google_id}/events/#{google_id}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.path(calendar_google_id)
|
32
|
+
"/calendar/v3/calendars/#{calendar_google_id}/events"
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.from_json(json, *attrs)
|
36
|
+
calendar = attrs[0].calendar
|
37
|
+
|
38
|
+
if json["kind"] == "calendar#events"
|
39
|
+
(json["items"] || []).map { |e| from_json(e, *attrs) }
|
40
|
+
elsif json["kind"] == "calendar#event"
|
41
|
+
event = self.new(calendar)
|
42
|
+
event.google_id = json["id"]
|
43
|
+
event.etag = json["etag"]
|
44
|
+
event.description = json["description"]
|
45
|
+
event.summary = json["summary"]
|
46
|
+
event.visibility = json["visibility"]
|
47
|
+
event.status = json["status"]
|
48
|
+
|
49
|
+
start_time = json["start"]["dateTime"] || json["start"]["date"]
|
50
|
+
if start_time
|
51
|
+
event.start_time = Time.parse(start_time)
|
52
|
+
event.start_time_zone = json["start"]["timeZone"]
|
53
|
+
end
|
54
|
+
|
55
|
+
end_time = json["end"]["dateTime"] || json["end"]["date"]
|
56
|
+
if end_time
|
57
|
+
event.end_time = Time.parse(end_time)
|
58
|
+
event.end_time_zone = json["end"]["timeZone"]
|
59
|
+
end
|
60
|
+
|
61
|
+
event.updated = Time.parse(json["updated"]) if json['updated']
|
62
|
+
event.created = Time.parse(json["created"]) if json['created']
|
63
|
+
event
|
64
|
+
else
|
65
|
+
raise "Not implemented:\n#{json.inspect}"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def to_google(yajl_opts = {})
|
70
|
+
hash = {
|
71
|
+
"kind" => "calendar#event",
|
72
|
+
}
|
73
|
+
hash["etag"] = etag if etag
|
74
|
+
hash["id"] = google_id if google_id
|
75
|
+
hash["description"] = description if description
|
76
|
+
hash["summary"] = summary if summary
|
77
|
+
start = {}
|
78
|
+
start["dateTime"] = format_time(start_time) if start_time
|
79
|
+
start["date"] = nil if start_time
|
80
|
+
start["timeZone"] = start_time_zone if start_time_zone
|
81
|
+
finish = {}
|
82
|
+
finish["dateTime"] = format_time(end_time) if end_time
|
83
|
+
finish["date"] = nil if end_time
|
84
|
+
finish["timeZone"] = end_time_zone if end_time_zone
|
85
|
+
hash["start"] = start
|
86
|
+
hash["end"] = finish
|
87
|
+
hash["visibility"] = visibility if visibility
|
88
|
+
hash["status"] = status if status
|
89
|
+
Yajl::Encoder.encode(hash, yajl_opts)
|
90
|
+
end
|
91
|
+
|
92
|
+
def new?
|
93
|
+
self.google_id.nil?
|
94
|
+
end
|
95
|
+
|
96
|
+
def format_time(time)
|
97
|
+
time.strftime("%Y-%m-%dT%H:%M:%S%:z")
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
class GoogleR
|
2
|
+
class Error < Exception
|
3
|
+
attr_reader :status, :response
|
4
|
+
|
5
|
+
def initialize(status, response)
|
6
|
+
@status, @response = status, response
|
7
|
+
end
|
8
|
+
|
9
|
+
def message
|
10
|
+
[
|
11
|
+
"Response code: #{@status}",
|
12
|
+
"Response body: #{@response}",
|
13
|
+
].join("\n")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
|
3
|
+
class GoogleR::Group
|
4
|
+
Property = Struct.new(:name, :info)
|
5
|
+
|
6
|
+
attr_accessor :property, :etag, :google_id, :title, :updated
|
7
|
+
|
8
|
+
def self.url
|
9
|
+
"https://www.google.com"
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.path
|
13
|
+
"/m8/feeds/groups/default/full/"
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.api_headers
|
17
|
+
{
|
18
|
+
'GData-Version' => '3.0',
|
19
|
+
'Content-Type' => 'application/atom+xml',
|
20
|
+
}
|
21
|
+
end
|
22
|
+
|
23
|
+
def path
|
24
|
+
if new?
|
25
|
+
self.class.path
|
26
|
+
else
|
27
|
+
self.class.path + google_id.split("/")[-1]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_google
|
32
|
+
builder = Nokogiri::XML::Builder.new(:encoding => "UTF-8") do |xml|
|
33
|
+
root_attrs = {
|
34
|
+
'xmlns:atom' => 'http://www.w3.org/2005/Atom',
|
35
|
+
'xmlns:gd' => 'http://schemas.google.com/g/2005',
|
36
|
+
}
|
37
|
+
root_attrs["gd:etag"] = self.etag unless new?
|
38
|
+
xml.entry(root_attrs) do
|
39
|
+
xml.id_ self.google_id unless new?
|
40
|
+
xml.updated self.updated.strftime("%Y-%m-%dT%H:%M:%S.%LZ") unless self.updated.nil?
|
41
|
+
|
42
|
+
xml['atom'].title({:type => "text"}, self.title) unless self.title.nil?
|
43
|
+
if self.property
|
44
|
+
xml['gd'].extendedProperty({:name => self.property.name}) do
|
45
|
+
xml.info self.property.info unless self.property.info.nil?
|
46
|
+
end
|
47
|
+
end
|
48
|
+
xml.parent.namespace = xml.parent.namespace_definitions.find { |ns| ns.prefix == "atom" }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
builder.to_xml
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.from_xml(doc, *attrs)
|
55
|
+
is_collection = doc.search("totalResults").size > 0
|
56
|
+
return doc.search("entry").map { |e| from_xml(e) } if is_collection
|
57
|
+
|
58
|
+
group = self.new
|
59
|
+
|
60
|
+
google_id = doc.search("id")
|
61
|
+
if google_id.empty?
|
62
|
+
group.etag = group.google_id = nil
|
63
|
+
else
|
64
|
+
group.etag = doc["etag"]
|
65
|
+
group.google_id = google_id.inner_text
|
66
|
+
end
|
67
|
+
|
68
|
+
title = doc.search("title")
|
69
|
+
group.title = title.inner_text unless title.size == 0
|
70
|
+
|
71
|
+
updated = doc.search("updated")
|
72
|
+
group.updated = Time.parse(updated.inner_text) unless updated.empty?
|
73
|
+
|
74
|
+
extended = doc.search("extendedProperty")
|
75
|
+
if extended.size != 0
|
76
|
+
info = extended.search("info")
|
77
|
+
info = info.size == 0 ? nil : info.inner_text
|
78
|
+
property = GoogleR::Group::Property.new(extended[0][:name], info)
|
79
|
+
group.property = property
|
80
|
+
end
|
81
|
+
|
82
|
+
group
|
83
|
+
end
|
84
|
+
|
85
|
+
def new?
|
86
|
+
self.google_id.nil?
|
87
|
+
end
|
88
|
+
end
|