aeden-contacts 0.2.15

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.
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