aeden-contacts 0.2.15

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.rdoc +51 -0
  3. data/Rakefile +71 -0
  4. data/VERSION.yml +4 -0
  5. data/lib/config/contacts.yml +10 -0
  6. data/lib/contacts/flickr.rb +133 -0
  7. data/lib/contacts/google.rb +387 -0
  8. data/lib/contacts/google_oauth.rb +91 -0
  9. data/lib/contacts/version.rb +9 -0
  10. data/lib/contacts/windows_live.rb +164 -0
  11. data/lib/contacts/yahoo.rb +236 -0
  12. data/lib/contacts.rb +55 -0
  13. data/spec/contact_spec.rb +61 -0
  14. data/spec/feeds/contacts.yml +10 -0
  15. data/spec/feeds/flickr/auth.getFrob.xml +4 -0
  16. data/spec/feeds/flickr/auth.getToken.xml +5 -0
  17. data/spec/feeds/google-many.xml +48 -0
  18. data/spec/feeds/google-single.xml +46 -0
  19. data/spec/feeds/wl_contacts.xml +29 -0
  20. data/spec/feeds/yh_contacts.txt +119 -0
  21. data/spec/feeds/yh_credential.xml +28 -0
  22. data/spec/flickr/auth_spec.rb +80 -0
  23. data/spec/gmail/auth_spec.rb +70 -0
  24. data/spec/gmail/fetching_spec.rb +198 -0
  25. data/spec/rcov.opts +2 -0
  26. data/spec/spec.opts +2 -0
  27. data/spec/spec_helper.rb +84 -0
  28. data/spec/windows_live/windows_live_spec.rb +34 -0
  29. data/spec/yahoo/yahoo_spec.rb +83 -0
  30. data/vendor/fakeweb/CHANGELOG +80 -0
  31. data/vendor/fakeweb/LICENSE.txt +281 -0
  32. data/vendor/fakeweb/README.rdoc +160 -0
  33. data/vendor/fakeweb/Rakefile +57 -0
  34. data/vendor/fakeweb/fakeweb.gemspec +13 -0
  35. data/vendor/fakeweb/lib/fake_web/ext/net_http.rb +58 -0
  36. data/vendor/fakeweb/lib/fake_web/registry.rb +78 -0
  37. data/vendor/fakeweb/lib/fake_web/responder.rb +88 -0
  38. data/vendor/fakeweb/lib/fake_web/response.rb +10 -0
  39. data/vendor/fakeweb/lib/fake_web/socket_delegator.rb +24 -0
  40. data/vendor/fakeweb/lib/fake_web.rb +152 -0
  41. data/vendor/fakeweb/test/fixtures/test_example.txt +1 -0
  42. data/vendor/fakeweb/test/fixtures/test_request +21 -0
  43. data/vendor/fakeweb/test/test_allow_net_connect.rb +41 -0
  44. data/vendor/fakeweb/test/test_fake_web.rb +453 -0
  45. data/vendor/fakeweb/test/test_fake_web_open_uri.rb +62 -0
  46. data/vendor/fakeweb/test/test_helper.rb +52 -0
  47. data/vendor/fakeweb/test/test_query_string.rb +37 -0
  48. data/vendor/windowslivelogin.rb +1151 -0
  49. metadata +108 -0
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 Mislav Marohnić
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
17
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
18
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
19
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
20
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,51 @@
1
+ == Install
2
+
3
+ gem install contacts --source http://gems.github.com
4
+
5
+ == Basic usage instructions
6
+
7
+ Fetch users' contact lists from your web application without asking them to
8
+ provide their passwords.
9
+
10
+ First, register[http://code.google.com/apis/accounts/docs/RegistrationForWebAppsAuto.html]
11
+ your application's domain. Then make users follow this URL:
12
+
13
+ Contacts::Google.authentication_url('http://mysite.com/invite')
14
+
15
+ They will authenticate on Google and it will send them back to the URL
16
+ provided. Google will add a token GET parameter to the query part of the URL.
17
+ Use that token in the next step:
18
+
19
+ gmail = Contacts::Google.new('example@gmail.com', params[:token])
20
+ contacts = gmail.contacts
21
+ #-> [ ['Fitzgerald', 'fubar@gmail.com', 'fubar@example.com'],
22
+ ['William Paginate', 'will.paginate@gmail.com'], ...
23
+ ]
24
+
25
+ Read more in Contacts::Google. I plan to support more APIs (Microsoft Live, for
26
+ starters); feel free to contribute.
27
+
28
+ Author: <b>Mislav Marohnić</b> (mislav.marohnic@gmail.com)
29
+
30
+ == Documentation auto-generated from specifications
31
+
32
+ Contacts::Google.authentication_url
33
+ - generates a URL for target with default parameters
34
+ - should handle boolean parameters
35
+ - skips parameters that have nil value
36
+ - should be able to exchange one-time for session token
37
+
38
+ Contacts::Google
39
+ - fetches contacts feed via HTTP GET
40
+ - handles a normal response body
41
+ - handles gzipped response
42
+ - raises a FetchingError when something goes awry
43
+ - parses the resulting feed into name/email pairs
44
+ - parses a complex feed into name/email pairs
45
+ - makes modification time available after parsing
46
+
47
+ Contacts::Google GET query parameter handling
48
+ - abstracts ugly parameters behind nicer ones
49
+ - should have implicit :descending with :order
50
+ - should have default :limit of 200
51
+ - should skip nil values in parameters
data/Rakefile ADDED
@@ -0,0 +1,71 @@
1
+ require 'spec/rake/spectask'
2
+ require 'rake/rdoctask'
3
+
4
+ task :default => :spec
5
+
6
+ spec_opts = 'spec/spec.opts'
7
+ spec_glob = FileList['spec/**/*_spec.rb']
8
+ libs = ['lib', 'spec', 'vendor/fakeweb/lib']
9
+
10
+ desc 'Run all specs in spec directory'
11
+ Spec::Rake::SpecTask.new(:spec) do |t|
12
+ t.libs = libs
13
+ t.spec_opts = ['--options', spec_opts]
14
+ t.spec_files = spec_glob
15
+ # t.warning = true
16
+ end
17
+
18
+ namespace :spec do
19
+ desc 'Analyze spec coverage with RCov'
20
+ Spec::Rake::SpecTask.new(:rcov) do |t|
21
+ t.libs = libs
22
+ t.spec_files = spec_glob
23
+ t.spec_opts = ['--options', spec_opts]
24
+ t.rcov = true
25
+ t.rcov_opts = lambda do
26
+ IO.readlines('spec/rcov.opts').map { |l| l.chomp.split(" ") }.flatten
27
+ end
28
+ end
29
+
30
+ desc 'Print Specdoc for all specs'
31
+ Spec::Rake::SpecTask.new(:doc) do |t|
32
+ t.libs = libs
33
+ t.spec_opts = ['--format', 'specdoc', '--dry-run']
34
+ t.spec_files = spec_glob
35
+ end
36
+
37
+ desc 'Generate HTML report'
38
+ Spec::Rake::SpecTask.new(:html) do |t|
39
+ t.libs = libs
40
+ t.spec_opts = ['--format', 'html:doc/spec.html', '--diff']
41
+ t.spec_files = spec_glob
42
+ t.fail_on_error = false
43
+ end
44
+ end
45
+
46
+ desc 'Generate RDoc documentation'
47
+ Rake::RDocTask.new(:rdoc) do |rdoc|
48
+ rdoc.rdoc_files.add ['README.rdoc', 'MIT-LICENSE', 'lib/**/*.rb']
49
+ rdoc.main = 'README.rdoc'
50
+ rdoc.title = 'Ruby Contacts library'
51
+
52
+ rdoc.rdoc_dir = 'doc'
53
+ rdoc.options << '--inline-source'
54
+ rdoc.options << '--charset=UTF-8'
55
+ end
56
+
57
+ begin
58
+ require 'jeweler'
59
+ Jeweler::Tasks.new do |s|
60
+ s.name = "contacts"
61
+ s.summary = "Ruby library for consuming Google, Yahoo!, Flickr and Windows Live contact APIs"
62
+ s.email = "anthonyeden@gmail.com"
63
+ s.homepage = "http://github.com/aeden/contacts"
64
+ s.description = "TODO"
65
+ s.authors = ["Mislav Marohnić", "Lukas Fittl", "Keavy Miller"]
66
+ s.files = FileList["[A-Z]*", "{lib,spec,vendor}/**/*"]
67
+ end
68
+ rescue LoadError
69
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
70
+ end
71
+
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :minor: 2
3
+ :patch: 15
4
+ :major: 0
@@ -0,0 +1,10 @@
1
+ windows_live:
2
+ appid: your_app_id
3
+ secret: your_app_secret_key
4
+ security_algorithm: wsignin1.0
5
+ return_url: http://yourserver.com/your_return_url
6
+ policy_url: http://yourserver.com/you_policy_url
7
+
8
+ yahoo:
9
+ appid: your_app_id
10
+ secret: your_shared_secret
@@ -0,0 +1,133 @@
1
+ require 'contacts'
2
+
3
+ require 'rubygems'
4
+ require 'hpricot'
5
+ require 'md5'
6
+ require 'cgi'
7
+ require 'time'
8
+ require 'zlib'
9
+ require 'stringio'
10
+ require 'net/http'
11
+
12
+ module Contacts
13
+
14
+ class Flickr
15
+ DOMAIN = 'api.flickr.com'
16
+ ServicesPath = '/services/rest/'
17
+
18
+ def self.frob_url(key, secret)
19
+ url_for(:api_key => key, :secret => secret, :method => 'flickr.auth.getFrob')
20
+ end
21
+
22
+ def self.frob_from_response(response)
23
+ doc = Hpricot::XML response.body
24
+ doc.at('frob').inner_text
25
+ end
26
+
27
+ def self.authentication_url_for_frob(frob, key, secret)
28
+ params = { :api_key => key, :secret => secret, :perms => 'read', :frob => frob }
29
+ 'http://www.flickr.com/services/auth/?' + query_string(params)
30
+ end
31
+
32
+ def self.authentication_url(key, secret)
33
+ response = http_start do |flickr|
34
+ flickr.get(frob_url(key, secret))
35
+ end
36
+ authentication_url_for_frob(frob_from_response(response), key, secret)
37
+ end
38
+
39
+ def self.token_url(key, secret, frob)
40
+ params = { :api_key => key, :secret => secret, :frob => frob, :method => 'flickr.auth.getToken' }
41
+ url_for(params)
42
+ end
43
+
44
+ def self.get_token_from_frob(key, secret, frob)
45
+ response = http_start do |flickr|
46
+ flickr.get(token_url(key, secret, frob))
47
+ end
48
+ doc = Hpricot::XML response.body
49
+ doc.at('token').inner_text
50
+ end
51
+
52
+ private
53
+ # Use the key-sorted version of the parameters to construct
54
+ # a string, to which the secret is prepended.
55
+
56
+ def self.sort_params(params)
57
+ params.sort do |a,b|
58
+ a.to_s <=> b.to_s
59
+ end
60
+ end
61
+
62
+ def self.string_to_sign(params, secret)
63
+ string_to_sign = secret + sort_params(params).inject('') do |str, pair|
64
+ key, value = pair
65
+ str + key.to_s + value.to_s
66
+ end
67
+ end
68
+
69
+ # Get the MD5 digest of the string to sign
70
+ def self.get_signature(params, secret)
71
+ ::Digest::MD5.hexdigest(string_to_sign(params, secret))
72
+ end
73
+
74
+ def self.query_string(params)
75
+ secret = params.delete(:secret)
76
+ params[:api_sig] = get_signature(params, secret)
77
+
78
+ params.inject([]) do |arr, pair|
79
+ key, value = pair
80
+ arr << "#{key}=#{value}"
81
+ end.join('&')
82
+ end
83
+
84
+ def self.url_for(params)
85
+ ServicesPath + '?' + query_string(params)
86
+ end
87
+
88
+ def self.http_start(ssl = false)
89
+ port = ssl ? Net::HTTP::https_default_port : Net::HTTP::http_default_port
90
+ http = Net::HTTP.new(DOMAIN, port)
91
+ redirects = 0
92
+ if ssl
93
+ http.use_ssl = true
94
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
95
+ end
96
+ http.start
97
+
98
+ begin
99
+ response = yield(http)
100
+
101
+ loop do
102
+ inspect_response(response) if Contacts::verbose?
103
+
104
+ case response
105
+ when Net::HTTPSuccess
106
+ break response
107
+ when Net::HTTPRedirection
108
+ if redirects == TooManyRedirects::MAX_REDIRECTS
109
+ raise TooManyRedirects.new(response)
110
+ end
111
+ location = URI.parse response['Location']
112
+ puts "Redirected to #{location}"
113
+ response = http.get(location.path)
114
+ redirects += 1
115
+ else
116
+ response.error!
117
+ end
118
+ end
119
+ ensure
120
+ http.finish
121
+ end
122
+ end
123
+
124
+ def self.inspect_response(response, out = $stderr)
125
+ out.puts response.inspect
126
+ for name, value in response
127
+ out.puts "#{name}: #{value}"
128
+ end
129
+ out.puts "----\n#{response.body}\n----" unless response.body.empty?
130
+ end
131
+ end
132
+
133
+ end
@@ -0,0 +1,387 @@
1
+ require 'contacts'
2
+
3
+ require 'rubygems'
4
+ require 'hpricot'
5
+ require 'cgi'
6
+ require 'time'
7
+ require 'zlib'
8
+ require 'stringio'
9
+ require 'net/http'
10
+ require 'net/https'
11
+
12
+ module Contacts
13
+ # == Fetching Google Contacts
14
+ #
15
+ # First, get the user to follow the following URL:
16
+ #
17
+ # Contacts::Google.authentication_url('http://mysite.com/invite')
18
+ #
19
+ # After he authenticates successfully to Google, it will redirect him back to the target URL
20
+ # (specified as argument above) and provide the token GET parameter. Use it to create a
21
+ # new instance of this class and request the contact list:
22
+ #
23
+ # gmail = Contacts::Google.new(params[:token])
24
+ # contacts = gmail.contacts
25
+ # #-> [ ['Fitzgerald', 'fubar@gmail.com', 'fubar@example.com'],
26
+ # ['William Paginate', 'will.paginate@gmail.com'], ...
27
+ # ]
28
+ #
29
+ # == Storing a session token
30
+ #
31
+ # The basic token that you will get after the user has authenticated on Google is valid
32
+ # for <b>only one request</b>. However, you can specify that you want a session token which
33
+ # doesn't expire:
34
+ #
35
+ # Contacts::Google.authentication_url('http://mysite.com/invite', :session => true)
36
+ #
37
+ # When the user authenticates, he will be redirected back with a token that can be exchanged
38
+ # for a session token with the following method:
39
+ #
40
+ # token = Contacts::Google.sesion_token(params[:token])
41
+ #
42
+ # Now you have a permanent token. Store it with other user data so you can query the API
43
+ # on his behalf without him having to authenticate on Google each time.
44
+ class Google
45
+ DOMAIN = 'www.google.com'
46
+ AuthSubPath = '/accounts/AuthSub' # all variants go over HTTPS
47
+ ClientLogin = '/accounts/ClientLogin'
48
+ FeedsPath = '/m8/feeds/contacts/'
49
+
50
+ # default options for #authentication_url
51
+ def self.authentication_url_options
52
+ @authentication_url_options ||= {
53
+ :scope => "http://#{DOMAIN}#{FeedsPath}",
54
+ :secure => false,
55
+ :session => false
56
+ }
57
+ end
58
+
59
+ # default options for #client_login
60
+ def self.client_login_options
61
+ @client_login_options ||= {
62
+ :accountType => 'GOOGLE',
63
+ :service => 'cp',
64
+ :source => 'Contacts-Ruby'
65
+ }
66
+ end
67
+
68
+ # URL to Google site where user authenticates. Afterwards, Google redirects to your
69
+ # site with the URL specified as +target+.
70
+ #
71
+ # Options are:
72
+ # * <tt>:scope</tt> -- the AuthSub scope in which the resulting token is valid
73
+ # (default: "http://www.google.com/m8/feeds/contacts/")
74
+ # * <tt>:secure</tt> -- boolean indicating whether the token will be secure. Only available
75
+ # for registered domains.
76
+ # (default: false)
77
+ # * <tt>:session</tt> -- boolean indicating if the token can be exchanged for a session token
78
+ # (default: false)
79
+ def self.authentication_url(target, options = {})
80
+ params = authentication_url_options.merge(options)
81
+ params[:next] = target
82
+ query = query_string(params)
83
+ "https://#{DOMAIN}#{AuthSubPath}Request?#{query}"
84
+ end
85
+
86
+ # Makes an HTTPS request to exchange the given token with a session one. Session
87
+ # tokens never expire, so you can store them in the database alongside user info.
88
+ #
89
+ # Returns the new token as string or nil if the parameter couldn't be found in response
90
+ # body.
91
+ def self.session_token(token)
92
+ response = http_start do |google|
93
+ google.get(AuthSubPath + 'SessionToken', authorization_header(token))
94
+ end
95
+
96
+ pair = response.body.split(/\n/).detect { |p| p.index('Token=') == 0 }
97
+ pair.split('=').last if pair
98
+ end
99
+
100
+ # Alternative to AuthSub: using email and password.
101
+ def self.client_login(email, password)
102
+ response = http_start do |google|
103
+ query = query_string(client_login_options.merge(:Email => email, :Passwd => password))
104
+ puts "posting #{query} to #{ClientLogin}" if Contacts::verbose?
105
+ google.post(ClientLogin, query)
106
+ end
107
+
108
+ pair = response.body.split(/\n/).detect { |p| p.index('Auth=') == 0 }
109
+ pair.split('=').last if pair
110
+ end
111
+
112
+ attr_reader :user, :token, :headers
113
+ attr_accessor :projection
114
+
115
+ # A token is required here. By default, an AuthSub token from
116
+ # Google is one-time only, which means you can only make a single request with it.
117
+ def initialize(token, user_id = 'default', client = false)
118
+ @user = user_id.to_s
119
+ @token = token.to_s
120
+ @headers = {
121
+ 'Accept-Encoding' => 'gzip',
122
+ 'User-Agent' => Identifier + ' (gzip)'
123
+ }.update(self.class.authorization_header(@token, client))
124
+ @projection = 'thin'
125
+ end
126
+
127
+ def get(params) # :nodoc:
128
+ self.class.http_start(false) do |google|
129
+ path = FeedsPath + CGI.escape(@user)
130
+ google_params = translate_parameters(params)
131
+ query = self.class.query_string(google_params)
132
+ google.get("#{path}/#{@projection}?#{query}", @headers)
133
+ end
134
+ end
135
+
136
+ # Timestamp of last update. This value is available only after the XML
137
+ # document has been parsed; for instance after fetching the contact list.
138
+ def updated_at
139
+ @updated_at ||= Time.parse @updated_string if @updated_string
140
+ end
141
+
142
+ # Timestamp of last update as it appeared in the XML document
143
+ def updated_at_string
144
+ @updated_string
145
+ end
146
+
147
+ # Fetches, parses and returns the contact list.
148
+ #
149
+ # ==== Options
150
+ # * <tt>:limit</tt> -- use a large number to fetch a bigger contact list (default: 200)
151
+ # * <tt>:offset</tt> -- 0-based value, can be used for pagination
152
+ # * <tt>:order</tt> -- currently the only value support by Google is "lastmodified"
153
+ # * <tt>:descending</tt> -- boolean
154
+ # * <tt>:updated_after</tt> -- string or time-like object, use to only fetch contacts
155
+ # that were updated after this date
156
+ def contacts(options = {})
157
+ params = { :limit => 200 }.update(options)
158
+ response = get(params)
159
+ parse_contacts response_body(response)
160
+ end
161
+
162
+ # Fetches contacts using multiple API calls when necessary
163
+ def all_contacts(options = {}, chunk_size = 200)
164
+ in_chunks(options, :contacts, chunk_size)
165
+ end
166
+
167
+ def response_body(response)
168
+ self.class.response_body(response)
169
+ end
170
+
171
+ def self.response_body(response)
172
+ unless response['Content-Encoding'] == 'gzip'
173
+ response.body
174
+ else
175
+ gzipped = StringIO.new(response.body)
176
+ Zlib::GzipReader.new(gzipped).read
177
+ end
178
+ end
179
+
180
+ protected
181
+
182
+ def in_chunks(options, what, chunk_size)
183
+ returns = []
184
+ offset = 0
185
+
186
+ begin
187
+ chunk = send(what, options.merge(:offset => offset, :limit => chunk_size))
188
+ returns.push(*chunk)
189
+ offset += chunk_size
190
+ end while chunk.size == chunk_size
191
+
192
+ returns
193
+ end
194
+
195
+ def parse_contacts(body)
196
+ doc = Hpricot::XML body
197
+ contacts_found = []
198
+
199
+ if updated_node = doc.at('/feed/updated')
200
+ @updated_string = updated_node.inner_text
201
+ end
202
+
203
+ (doc / '/feed/entry').each do |entry|
204
+ title_node = entry.at('/title')
205
+ id_node = entry.at('/id')
206
+ email_nodes = entry / 'gd:email'
207
+ im_nodes = entry / 'gd:im'
208
+ phone_nodes = entry / 'gd:phoneNumber'
209
+ address_nodes = entry / 'gd:postalAddress'
210
+ organization_nodes = entry / 'gd:organization'
211
+ content_node = entry / 'atom:content'
212
+
213
+ service_id = id_node ? id_node.inner_text : nil
214
+ name = title_node ? title_node.inner_text : nil
215
+
216
+ contact = Contact.new(nil, name)
217
+ contact.service_id = service_id
218
+ email_nodes.each do |n|
219
+ contact.emails << {
220
+ 'value' => n['address'].to_s,
221
+ 'type' => (type_map[n['rel']] || 'other').to_s,
222
+ 'primary' => (n['primary'] == 'true').to_s
223
+ }
224
+ end
225
+ im_nodes.each do |n|
226
+ contact.ims << {
227
+ 'value' => n['address'].to_s,
228
+ 'type' => (im_protocols[n['protocol']] || 'unknown').to_s
229
+ }
230
+ end
231
+ phone_nodes.each do |n|
232
+ contact.phones << {
233
+ 'value' => n.inner_text,
234
+ 'type' => (type_map[n['rel']] || 'other').to_s
235
+ }
236
+ end
237
+ address_nodes.each do |n|
238
+ contact.addresses << {
239
+ 'formatted' => n.inner_text,
240
+ 'type' => (type_map[n['rel']] || 'other').to_s
241
+ }
242
+ end
243
+ organization_nodes.each do |n|
244
+ if n['rel'] == 'http://schemas.google.com/g/2005#work'
245
+ org_name = n / 'gd:orgName'
246
+ org_title = n / 'gd:orgTitle'
247
+ org_department = n / 'gd:orgDepartment'
248
+ org_description = n / 'gd:orgJobDescription'
249
+
250
+ contact.organizations << {
251
+ 'type' => 'job',
252
+ 'name' => org_name ? org_name.inner_text : '',
253
+ 'title' => org_title ? org_title.inner_text : '',
254
+ 'department' => org_department ? org_department.inner_text : '',
255
+ 'description' => org_description ? org_description.inner_text : ''
256
+ }
257
+ end
258
+ end
259
+ contact.note = content_node ? content_node.inner_text : ''
260
+
261
+ contacts_found << contact
262
+ end
263
+
264
+ contacts_found
265
+ end
266
+
267
+ def type_map
268
+ @type_map ||= {
269
+ 'http://schemas.google.com/g/2005#other' => 'other',
270
+ 'http://schemas.google.com/g/2005#home' => 'home',
271
+ 'http://schemas.google.com/g/2005#work' => 'work',
272
+ 'http://schemas.google.com/g/2005#mobile' => 'mobile',
273
+ 'http://schemas.google.com/g/2005#pager' => 'pager',
274
+ 'http://schemas.google.com/g/2005#fax' => 'fax',
275
+ 'http://schemas.google.com/g/2005#work_fax' => 'fax',
276
+ 'http://schemas.google.com/g/2005#home_fax' => 'fax'
277
+ }
278
+ end
279
+
280
+ def im_protocols
281
+ @im_protocols ||= {
282
+ 'http://schemas.google.com/g/2005#GOOGLE_TALK' => 'google',
283
+ 'http://schemas.google.com/g/2005#YAHOO' => 'yahoo',
284
+ 'http://schemas.google.com/g/2005#SKYPE' => 'skype',
285
+ 'http://schemas.google.com/g/2005#JABBER' => 'jabber',
286
+ 'http://schemas.google.com/g/2005#MSN' => 'msn',
287
+ 'http://schemas.google.com/g/2005#QQ' => 'qq',
288
+ 'http://schemas.google.com/g/2005#ICQ' => 'icq',
289
+ 'http://schemas.google.com/g/2005#AIM' => 'aim'
290
+ }
291
+ end
292
+
293
+ # Constructs a query string from a Hash object
294
+ def self.query_string(params)
295
+ params.inject([]) do |all, pair|
296
+ key, value = pair
297
+ unless value.nil?
298
+ value = case value
299
+ when TrueClass; '1'
300
+ when FalseClass; '0'
301
+ else value
302
+ end
303
+
304
+ all << "#{CGI.escape(key.to_s)}=#{CGI.escape(value.to_s)}"
305
+ end
306
+ all
307
+ end.join('&')
308
+ end
309
+
310
+ def translate_parameters(params)
311
+ params.inject({}) do |all, pair|
312
+ key, value = pair
313
+ unless value.nil?
314
+ key = case key
315
+ when :limit
316
+ 'max-results'
317
+ when :offset
318
+ value = value.to_i + 1
319
+ 'start-index'
320
+ when :order
321
+ all['sortorder'] = 'descending' if params[:descending].nil?
322
+ 'orderby'
323
+ when :descending
324
+ value = value ? 'descending' : 'ascending'
325
+ 'sortorder'
326
+ when :updated_after
327
+ value = value.strftime("%Y-%m-%dT%H:%M:%S%Z") if value.respond_to? :strftime
328
+ 'updated-min'
329
+ else key
330
+ end
331
+
332
+ all[key] = value
333
+ end
334
+ all
335
+ end
336
+ end
337
+
338
+ def self.authorization_header(token, client = false)
339
+ type = client ? 'GoogleLogin auth' : 'AuthSub token'
340
+ { 'Authorization' => %(#{type}="#{token}") }
341
+ end
342
+
343
+ def self.http_start(ssl = true)
344
+ port = ssl ? Net::HTTP::https_default_port : Net::HTTP::http_default_port
345
+ http = Net::HTTP.new(DOMAIN, port)
346
+ redirects = 0
347
+ if ssl
348
+ http.use_ssl = true
349
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
350
+ end
351
+ http.start
352
+
353
+ begin
354
+ response = yield(http)
355
+
356
+ loop do
357
+ inspect_response(response) if Contacts::verbose?
358
+
359
+ case response
360
+ when Net::HTTPSuccess
361
+ break response
362
+ when Net::HTTPRedirection
363
+ if redirects == TooManyRedirects::MAX_REDIRECTS
364
+ raise TooManyRedirects.new(response)
365
+ end
366
+ location = URI.parse response['Location']
367
+ puts "Redirected to #{location}"
368
+ response = http.get(location.path)
369
+ redirects += 1
370
+ else
371
+ response.error!
372
+ end
373
+ end
374
+ ensure
375
+ http.finish
376
+ end
377
+ end
378
+
379
+ def self.inspect_response(response, out = $stderr)
380
+ out.puts response.inspect
381
+ for name, value in response
382
+ out.puts "#{name}: #{value}"
383
+ end
384
+ out.puts "----\n#{response_body(response)}\n----" unless response.body.empty?
385
+ end
386
+ end
387
+ end