google_apps_api 0.1.0 → 0.2.1
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.
- data/.gitignore +1 -1
- data/VERSION +1 -1
- data/google_apps_api.gemspec +9 -4
- data/lib/config/calendar.yml +36 -9
- data/lib/config/contacts.yml +35 -0
- data/lib/config/profiles.yml +1 -0
- data/lib/config/provisioning.yml +10 -3
- data/lib/google_apps_api/base_api.rb +120 -36
- data/lib/google_apps_api/calendar.rb +305 -30
- data/lib/google_apps_api/contacts.rb +83 -53
- data/lib/google_apps_api/provisioning.rb +66 -242
- data/test/example_connection_config.yml +4 -0
- data/test/google_apps_api_base_api_test.rb +76 -0
- data/test/google_apps_api_calendar_test.rb +225 -54
- data/test/google_apps_api_contacts_test.rb +103 -27
- data/test/google_apps_api_off_domain_calendar_test.rb +27 -0
- data/test/google_apps_api_provisioning_test.rb +3 -4
- data/test/test_helper.rb +5 -0
- metadata +9 -4
- data/private/gapps-config.yml +0 -5
- data/private/userscalendars.xml +0 -113
data/.gitignore
CHANGED
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.1
|
1
|
+
0.2.1
|
data/google_apps_api.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{google_apps_api}
|
8
|
-
s.version = "0.1
|
8
|
+
s.version = "0.2.1"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["James Stuart"]
|
12
|
-
s.date = %q{2010-05-
|
12
|
+
s.date = %q{2010-05-25}
|
13
13
|
s.description = %q{APIs for Google Apps (currently Provisioning, Calendar, Calendar Resources)}
|
14
14
|
s.email = %q{tastyhat@jamesstuart.org}
|
15
15
|
s.extra_rdoc_files = [
|
@@ -25,6 +25,8 @@ Gem::Specification.new do |s|
|
|
25
25
|
"VERSION",
|
26
26
|
"google_apps_api.gemspec",
|
27
27
|
"lib/config/calendar.yml",
|
28
|
+
"lib/config/contacts.yml",
|
29
|
+
"lib/config/profiles.yml",
|
28
30
|
"lib/config/provisioning.yml",
|
29
31
|
"lib/google_apps_api.rb",
|
30
32
|
"lib/google_apps_api/base_api.rb",
|
@@ -35,11 +37,12 @@ Gem::Specification.new do |s|
|
|
35
37
|
"lib/google_apps_api/provisioning.rb",
|
36
38
|
"lib/google_apps_api/user_profiles.rb",
|
37
39
|
"lib/load_config.rb",
|
38
|
-
"
|
39
|
-
"private/userscalendars.xml",
|
40
|
+
"test/example_connection_config.yml",
|
40
41
|
"test/google_apps_api-calendar_resources_test.rb",
|
42
|
+
"test/google_apps_api_base_api_test.rb",
|
41
43
|
"test/google_apps_api_calendar_test.rb",
|
42
44
|
"test/google_apps_api_contacts_test.rb",
|
45
|
+
"test/google_apps_api_off_domain_calendar_test.rb",
|
43
46
|
"test/google_apps_api_provisioning_test.rb",
|
44
47
|
"test/google_apps_api_user_profiles_test.rb",
|
45
48
|
"test/helper.rb",
|
@@ -52,8 +55,10 @@ Gem::Specification.new do |s|
|
|
52
55
|
s.summary = %q{Various APIs for Google Apps}
|
53
56
|
s.test_files = [
|
54
57
|
"test/google_apps_api-calendar_resources_test.rb",
|
58
|
+
"test/google_apps_api_base_api_test.rb",
|
55
59
|
"test/google_apps_api_calendar_test.rb",
|
56
60
|
"test/google_apps_api_contacts_test.rb",
|
61
|
+
"test/google_apps_api_off_domain_calendar_test.rb",
|
57
62
|
"test/google_apps_api_provisioning_test.rb",
|
58
63
|
"test/google_apps_api_user_profiles_test.rb",
|
59
64
|
"test/helper.rb",
|
data/lib/config/calendar.yml
CHANGED
@@ -15,20 +15,47 @@
|
|
15
15
|
:method: :post
|
16
16
|
:path: ":auth:/accounts/ClientLogin"
|
17
17
|
:format: :text
|
18
|
-
:
|
18
|
+
:retrieve_calendar_for_user:
|
19
19
|
:method: :get
|
20
|
-
:path: ":feed_basic:/:username:/allcalendars/full"
|
21
|
-
:
|
22
|
-
|
23
|
-
|
20
|
+
:path: ":feed_basic:/:username:/allcalendars/full/:calendar:"
|
21
|
+
:format: GoogleAppsApi::CalendarEntity
|
22
|
+
:update_calendar_for_user:
|
23
|
+
:method: :put
|
24
|
+
:path: ":feed_basic:/:username:/allcalendars/full/:calendar:"
|
25
|
+
:format: GoogleAppsApi::CalendarEntity
|
26
|
+
:retrieve_calendars_for_user:
|
24
27
|
:method: :get
|
25
|
-
:path: ":feed_basic:/:username:/
|
28
|
+
:path: ":feed_basic:/:username:/allcalendars/full"
|
26
29
|
:feed: true
|
27
|
-
:
|
30
|
+
:format: GoogleAppsApi::CalendarEntity
|
28
31
|
:add_calendar_to_user:
|
29
32
|
:method: :post
|
30
33
|
:path: ":feed_basic:/:username:/allcalendars/full"
|
31
|
-
|
34
|
+
:format: GoogleAppsApi::CalendarEntity
|
35
|
+
:remove_calendar_from_user:
|
36
|
+
:method: :delete
|
37
|
+
:path: ":feed_basic:/:username:/allcalendars/full/:calendar:"
|
38
|
+
:format: :text
|
39
|
+
:retrieve_calendar_acl_for_user:
|
40
|
+
:method: :get
|
41
|
+
:path: ":feed_basic:/:calendar:/acl/full/:scope_type:%3a:scope_id:"
|
42
|
+
:format: GoogleAppsApi::CalendarAcl
|
43
|
+
:retrieve_acls_for_calendar:
|
44
|
+
:method: :get
|
45
|
+
:path: ":feed_basic:/:calendar:/acl/full"
|
46
|
+
:format: GoogleAppsApi::CalendarAcl
|
47
|
+
:feed: true
|
48
|
+
:create_calendar_acl:
|
49
|
+
:method: :post
|
50
|
+
:path: ":feed_basic:/:calendar:/acl/full"
|
51
|
+
:format: GoogleAppsApi::CalendarAcl
|
52
|
+
:set_calendar_acl:
|
53
|
+
:method: :put
|
54
|
+
:path: ":feed_basic:/:calendar:/acl/full/:scope:"
|
55
|
+
:format: GoogleAppsApi::CalendarAcl
|
56
|
+
:remove_calendar_acl:
|
32
57
|
:method: :delete
|
33
|
-
:path: ":feed_basic:/:
|
58
|
+
:path: ":feed_basic:/:calendar:/acl/full/:scope:"
|
59
|
+
:format: :text
|
60
|
+
|
34
61
|
|
@@ -0,0 +1,35 @@
|
|
1
|
+
:contacts:
|
2
|
+
:service: cp
|
3
|
+
|
4
|
+
:headers:
|
5
|
+
GData-Version: "2"
|
6
|
+
|
7
|
+
:action_subs:
|
8
|
+
:service: cl
|
9
|
+
:auth: https://www.google.com
|
10
|
+
:feed: https://www.google.com/m8/feeds/contacts
|
11
|
+
|
12
|
+
:action_hash:
|
13
|
+
:domain_login:
|
14
|
+
:method: :post
|
15
|
+
:path: ":auth:/accounts/ClientLogin"
|
16
|
+
:format: :text
|
17
|
+
:retrieve_all_contacts:
|
18
|
+
:method: :get
|
19
|
+
:path: ":feed:/:domain:/full"
|
20
|
+
:feed: true
|
21
|
+
:format: GoogleAppsApi::ContactEntity
|
22
|
+
:retrieve_contact:
|
23
|
+
:method: :get
|
24
|
+
:path: ":feed:/:domain:/full/:contact:"
|
25
|
+
:format: GoogleAppsApi::ContactEntity
|
26
|
+
:create_contact:
|
27
|
+
:method: :post
|
28
|
+
:path: ":feed:/:domain:/full"
|
29
|
+
:format: GoogleAppsApi::ContactEntity
|
30
|
+
:remove_contact:
|
31
|
+
:method: :delete
|
32
|
+
:path: ":feed:/:domain:/full/:contact:"
|
33
|
+
:format: :text
|
34
|
+
|
35
|
+
|
@@ -0,0 +1 @@
|
|
1
|
+
:profiles:
|
data/lib/config/provisioning.yml
CHANGED
@@ -12,25 +12,32 @@
|
|
12
12
|
:method: :post
|
13
13
|
:path: ":auth:/accounts/ClientLogin"
|
14
14
|
:format: :text
|
15
|
+
|
15
16
|
:rename_user:
|
16
17
|
:method: :put
|
17
18
|
:path: ":path_user:/:username:"
|
19
|
+
|
18
20
|
:delete_user:
|
19
21
|
:method: :delete
|
20
22
|
:path: ":path_user:/:username:"
|
23
|
+
|
21
24
|
:create_user:
|
22
25
|
:method: :post
|
23
26
|
:path: ":path_user:"
|
27
|
+
:format: GoogleAppsApi::UserEntity
|
28
|
+
|
24
29
|
:retrieve_user:
|
25
30
|
:method: :get
|
26
31
|
:path: ":path_user:/:username:"
|
27
|
-
:
|
32
|
+
:format: GoogleAppsApi::UserEntity
|
33
|
+
|
28
34
|
:retrieve_all_users:
|
29
35
|
:method: :get
|
30
36
|
:path: ":path_user:"
|
31
37
|
:feed: true
|
32
|
-
:
|
38
|
+
:format: GoogleAppsApi::UserEntity
|
39
|
+
|
33
40
|
:update_user:
|
34
41
|
:method: :put
|
35
42
|
:path: ":path_user:/:username:"
|
36
|
-
:format:
|
43
|
+
:format: GoogleAppsApi::UserEntity
|
@@ -1,7 +1,6 @@
|
|
1
|
-
include REXML
|
2
|
-
|
3
1
|
module GoogleAppsApi
|
4
2
|
class BaseApi
|
3
|
+
attr_reader :domain
|
5
4
|
|
6
5
|
|
7
6
|
def initialize(api_name, *args)
|
@@ -9,87 +8,97 @@ module GoogleAppsApi
|
|
9
8
|
options = args.extract_options!.merge!(api_config)
|
10
9
|
raise("Must supply admin_user") unless options[:admin_user]
|
11
10
|
raise("Must supply admin_password") unless options[:admin_password]
|
12
|
-
raise("Must supply domain")
|
11
|
+
@domain = options[:domain] || raise("Must supply domain")
|
13
12
|
@actions_hash = options[:action_hash] || raise("Must supply action hash")
|
14
13
|
@actions_subs = options[:action_subs] || raise("Must supply action subs")
|
15
14
|
@actions_hash[:next] = [:get, '']
|
16
|
-
@actions_subs[:domain] =
|
15
|
+
@actions_subs[:domain] = @domain
|
17
16
|
|
18
|
-
@token = login(options[:admin_user],
|
17
|
+
@token = login(options[:admin_user], @domain, options[:admin_password], options[:service])
|
19
18
|
@headers = {'Content-Type'=>'application/atom+xml', 'Authorization'=> 'GoogleLogin auth='+@token}.merge(options[:headers] || {})
|
20
19
|
|
21
20
|
end
|
22
21
|
|
23
|
-
def method_missing(method, *args, &block)
|
24
|
-
if @actions_hash.has_key?(method.to_sym)
|
25
|
-
request(method.to_sym, *args)
|
26
|
-
end
|
27
|
-
end
|
28
22
|
|
29
|
-
|
23
|
+
def entity(*args)
|
24
|
+
entity.merge(:domain => @domain)
|
25
|
+
end
|
30
26
|
|
31
|
-
def setup_action(*args)
|
32
|
-
options = args.extract_options!
|
33
|
-
actions = options[:action_hash]
|
34
27
|
|
35
|
-
|
36
|
-
actions[k] = {:method => v[0], :path => (v[1].to_s.gsub!(/\:([^\:]+)\:/) { |sub| options[sub.gsub(/\:/,"").to_sym] })}
|
37
|
-
end
|
38
|
-
|
39
|
-
return actions
|
40
|
-
end
|
28
|
+
private
|
41
29
|
|
42
30
|
def login(username, domain, password, service)
|
31
|
+
@gsession_id = nil
|
43
32
|
request_body = '&Email='+CGI.escape(username + "@" + domain)+'&Passwd='+CGI.escape(password)+'&accountType=HOSTED&service='+ service + '&source=columbiaUniversity-google_apps_api-0.1'
|
44
33
|
res = request(:domain_login, :headers => {'Content-Type'=>'application/x-www-form-urlencoded'}, :body => request_body)
|
45
34
|
|
46
35
|
|
47
36
|
return /^Auth=(.+)$/.match(res.to_s)[1]
|
48
37
|
end
|
38
|
+
|
39
|
+
|
49
40
|
|
50
41
|
def request(action, *args)
|
51
42
|
options = args.extract_options!
|
52
43
|
options = {:headers => @headers}.merge(options)
|
44
|
+
options[:headers] = (options[:headers] || {}).merge(options.delete(:merge_headers) || {})
|
53
45
|
action_hash = @actions_hash[action] || raise("invalid action #{action} called")
|
54
46
|
|
55
47
|
subs_hash = @actions_subs.merge(options)
|
56
48
|
subs_hash.each { |k,v| subs_hash[k] = action_gsub(v, subs_hash) if v.kind_of?(String)}
|
57
|
-
|
49
|
+
|
58
50
|
method = action_hash[:method]
|
59
51
|
path = action_gsub(action_hash[:path], subs_hash) + options[:query].to_s
|
60
52
|
is_feed = action_hash[:feed]
|
61
|
-
|
62
|
-
format =
|
53
|
+
format = options[:return_format] || action_hash[:format] || :xml
|
54
|
+
format = format.constantize unless [:xml, :text].include?(format) || format.kind_of?(Class)
|
55
|
+
|
56
|
+
|
57
|
+
if options[:debug]
|
58
|
+
puts "method: #{method}"
|
59
|
+
puts "path: #{path}"
|
60
|
+
puts "body: #{options[:body]}"
|
61
|
+
puts "headers: #{options[:headers]}"
|
62
|
+
puts "---\n"
|
63
|
+
end
|
64
|
+
|
63
65
|
response = http_request(method, path, options[:body], options[:headers])
|
64
66
|
|
65
67
|
if format == :text
|
68
|
+
puts response.body.content if options[:debug]
|
66
69
|
return response.body.content
|
67
70
|
else
|
68
71
|
begin
|
69
|
-
xml = Nokogiri::XML(response.body.content) { |c| c.strict}
|
72
|
+
xml = Nokogiri::XML(response.body.content) { |c| c.strict.noent}
|
73
|
+
|
70
74
|
test_errors(xml)
|
71
|
-
if
|
72
|
-
|
75
|
+
puts xml.to_s if options[:debug]
|
76
|
+
|
77
|
+
|
78
|
+
if format == :xml || !is_feed
|
79
|
+
format.kind_of?(Class) ? format.new(:xml => xml) : xml
|
80
|
+
else
|
81
|
+
entries = entryset(xml.css('feed>entry'), format)
|
73
82
|
|
74
83
|
|
75
84
|
while (next_feed = xml.at_css('feed>link[rel=next]'))
|
76
85
|
response = http_request(:get, next_feed.attribute("href").to_s, nil, options[:headers])
|
77
86
|
xml = Nokogiri::XML(response.body.content) { |c| c.strict}
|
78
|
-
entries += entryset(xml.css('feed>entry'),
|
87
|
+
entries += entryset(xml.css('feed>entry'),format)
|
79
88
|
end
|
80
89
|
|
81
90
|
entries
|
82
|
-
else
|
83
|
-
feed_class ? feed_class.new(xml) : xml
|
84
91
|
end
|
85
92
|
|
86
93
|
|
87
94
|
rescue Nokogiri::XML::SyntaxError => e
|
95
|
+
puts response.body.content if options[:debug]
|
96
|
+
|
88
97
|
error = GDataError.new()
|
89
98
|
error.code = "SyntaxError"
|
90
99
|
error.input = "path: #{path}"
|
91
|
-
error.reason = "
|
92
|
-
raise error,
|
100
|
+
error.reason = "#{response.body.content}"
|
101
|
+
raise error, error.inspect
|
93
102
|
end
|
94
103
|
end
|
95
104
|
end
|
@@ -97,15 +106,25 @@ module GoogleAppsApi
|
|
97
106
|
def http_request(method, path, body, headers, redirects = 0)
|
98
107
|
@hc ||= HTTPClient.new
|
99
108
|
|
109
|
+
path_with_gsession = path
|
110
|
+
|
111
|
+
if @gsession_id && redirects == 0
|
112
|
+
operator = path.include?("?") ? "&" : "?"
|
113
|
+
path_with_gsession += "#{operator}gsessionid=#{@gsession_id.to_s}"
|
114
|
+
end
|
115
|
+
|
100
116
|
response = case method
|
101
117
|
when :delete
|
102
|
-
@hc.send(method,
|
118
|
+
@hc.send(method, path_with_gsession, headers)
|
103
119
|
else
|
104
|
-
@hc.send(method,
|
120
|
+
@hc.send(method, path_with_gsession, body, headers)
|
105
121
|
end
|
106
122
|
|
107
123
|
if response.status_code == 302 && (redirects += 1) < 10
|
108
|
-
|
124
|
+
new_loc = response.header["Location"].to_s
|
125
|
+
gsession_match = new_loc.match(/gsessionid=([\w\-_]+)/)
|
126
|
+
@gsession_id = gsession_match[1].to_s if gsession_match
|
127
|
+
response = http_request(method, new_loc, body, headers, redirects)
|
109
128
|
end
|
110
129
|
return response
|
111
130
|
end
|
@@ -128,8 +147,8 @@ module GoogleAppsApi
|
|
128
147
|
end
|
129
148
|
end
|
130
149
|
|
131
|
-
def entryset(entries,
|
132
|
-
|
150
|
+
def entryset(entries, return_class)
|
151
|
+
return_class ? entries.collect { |en| return_class.new(:xml => en)} : entries
|
133
152
|
end
|
134
153
|
|
135
154
|
def escapeXML(text)
|
@@ -148,10 +167,75 @@ module GoogleAppsApi
|
|
148
167
|
|
149
168
|
end
|
150
169
|
|
170
|
+
|
171
|
+
|
172
|
+
|
173
|
+
class Entity
|
174
|
+
VALID_ENTITY_TYPES = [:user, :calendar, :domain, :contact]
|
175
|
+
|
176
|
+
attr_reader :kind, :id, :domain
|
177
|
+
def initialize(*args)
|
178
|
+
options = args.extract_options!
|
179
|
+
|
180
|
+
@kind = options.delete(:kind)
|
181
|
+
@id = options.delete(:id)
|
182
|
+
@domain = options.delete(:domain)
|
183
|
+
|
184
|
+
if (kind = options.keys.detect { |k| VALID_ENTITY_TYPES.include?(k.to_sym)})
|
185
|
+
@kind = kind.to_s
|
186
|
+
|
187
|
+
value = CGI::unescape(options[kind])
|
188
|
+
|
189
|
+
if value.include?("@")
|
190
|
+
@id, @domain = value.split("@",2)
|
191
|
+
else
|
192
|
+
@id = value
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
|
197
|
+
raise(ArgumentError, "Kind and Id must be specified") unless @kind && @id
|
198
|
+
end
|
199
|
+
|
200
|
+
def id_escaped
|
201
|
+
CGI::escape(@id)
|
202
|
+
end
|
203
|
+
|
204
|
+
def full_id
|
205
|
+
@id + (@domain.nil? ? "" : "@" + @domain)
|
206
|
+
end
|
207
|
+
|
208
|
+
def full_id_escaped
|
209
|
+
CGI::escape(full_id)
|
210
|
+
end
|
211
|
+
|
212
|
+
def qualified_id
|
213
|
+
@kind + ":" + full_id
|
214
|
+
end
|
215
|
+
|
216
|
+
def qualified_id_escaped
|
217
|
+
CGI::escape(qualified_id)
|
218
|
+
end
|
219
|
+
|
220
|
+
|
221
|
+
def ==(other)
|
222
|
+
other.kind_of?(Entity) && @kind == other.kind && @id == other.id && @domain == other.domain
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
151
226
|
class GDataError < RuntimeError
|
152
227
|
attr_accessor :code, :input, :reason
|
153
228
|
|
154
229
|
def initialize()
|
155
230
|
end
|
231
|
+
|
232
|
+
def to_s
|
233
|
+
"#{code}: #{reason}"
|
234
|
+
end
|
235
|
+
|
236
|
+
def inspect
|
237
|
+
"#{code}: #{reason}"
|
238
|
+
|
239
|
+
end
|
156
240
|
end
|
157
241
|
end
|