google_r 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|