pezra-contacts 0.1.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.
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,47 @@
1
+ == Basic usage instructions
2
+
3
+ Fetch users' contact lists from your web application without asking them to
4
+ provide their passwords.
5
+
6
+ First, register[http://code.google.com/apis/accounts/docs/RegistrationForWebAppsAuto.html]
7
+ your application's domain. Then make users follow this URL:
8
+
9
+ Contacts::Google.authentication_url('http://mysite.com/invite')
10
+
11
+ They will authenticate on Google and it will send them back to the URL
12
+ provided. Google will add a token GET parameter to the query part of the URL.
13
+ Use that token in the next step:
14
+
15
+ gmail = Contacts::Google.new('example@gmail.com', params[:token])
16
+ contacts = gmail.contacts
17
+ #-> [ ['Fitzgerald', 'fubar@gmail.com', 'fubar@example.com'],
18
+ ['William Paginate', 'will.paginate@gmail.com'], ...
19
+ ]
20
+
21
+ Read more in Contacts::Google. I plan to support more APIs (Microsoft Live, for
22
+ starters); feel free to contribute.
23
+
24
+ Author: <b>Mislav Marohnić</b> (mislav.marohnic@gmail.com)
25
+
26
+ == Documentation auto-generated from specifications
27
+
28
+ Contacts::Google.authentication_url
29
+ - generates a URL for target with default parameters
30
+ - should handle boolean parameters
31
+ - skips parameters that have nil value
32
+ - should be able to exchange one-time for session token
33
+
34
+ Contacts::Google
35
+ - fetches contacts feed via HTTP GET
36
+ - handles a normal response body
37
+ - handles gzipped response
38
+ - raises a FetchingError when something goes awry
39
+ - parses the resulting feed into name/email pairs
40
+ - parses a complex feed into name/email pairs
41
+ - makes modification time available after parsing
42
+
43
+ Contacts::Google GET query parameter handling
44
+ - abstracts ugly parameters behind nicer ones
45
+ - should have implicit :descending with :order
46
+ - should have default :limit of 200
47
+ - should skip nil values in parameters
data/Rakefile ADDED
@@ -0,0 +1,87 @@
1
+ require 'spec/rake/spectask'
2
+ require 'rake/rdoctask'
3
+ require 'pathname'
4
+
5
+ task :default => :spec
6
+
7
+ spec_opts = 'spec/spec.opts'
8
+ spec_glob = FileList['spec/**/*_spec.rb']
9
+ libs = ['lib', 'spec']
10
+
11
+ desc 'Run all specs in spec directory'
12
+ Spec::Rake::SpecTask.new(:spec) do |t|
13
+ t.libs = libs
14
+ t.spec_opts = ['--options', spec_opts]
15
+ t.spec_files = spec_glob
16
+ # t.warning = true
17
+ end
18
+
19
+ namespace :spec do
20
+ desc 'Analyze spec coverage with RCov'
21
+ Spec::Rake::SpecTask.new(:rcov) do |t|
22
+ t.libs = libs
23
+ t.spec_files = spec_glob
24
+ t.spec_opts = ['--options', spec_opts]
25
+ t.rcov = true
26
+ t.rcov_opts = lambda do
27
+ IO.readlines('spec/rcov.opts').map { |l| l.chomp.split(" ") }.flatten
28
+ end
29
+ end
30
+
31
+ desc 'Print Specdoc for all specs'
32
+ Spec::Rake::SpecTask.new(:doc) do |t|
33
+ t.libs = libs
34
+ t.spec_opts = ['--format', 'specdoc', '--dry-run']
35
+ t.spec_files = spec_glob
36
+ end
37
+
38
+ desc 'Generate HTML report'
39
+ Spec::Rake::SpecTask.new(:html) do |t|
40
+ t.libs = libs
41
+ t.spec_opts = ['--format', 'html:doc/spec.html', '--diff']
42
+ t.spec_files = spec_glob
43
+ t.fail_on_error = false
44
+ end
45
+ end
46
+
47
+ desc 'Generate RDoc documentation'
48
+ Rake::RDocTask.new(:rdoc) do |rdoc|
49
+ rdoc.rdoc_files.add ['README.rdoc', 'MIT-LICENSE', 'lib/**/*.rb']
50
+ rdoc.main = 'README.rdoc'
51
+ rdoc.title = 'Ruby Contacts library'
52
+
53
+ rdoc.rdoc_dir = 'doc'
54
+ rdoc.options << '--inline-source'
55
+ rdoc.options << '--charset=UTF-8'
56
+ end
57
+
58
+ desc "Generate gemspec"
59
+ task "gemspec" do
60
+
61
+ Dir.chdir(Pathname(__FILE__).dirname) do
62
+ Gem::Specification.new do |s|
63
+ s.name = "contacts"
64
+ s.version = "0.1.0"
65
+ s.date = Time.now
66
+ s.summary = "Online contact APIs library"
67
+ s.email = "mislav.marohnic@gmail.com"
68
+ s.homepage = "http://github.com/mislav/contacts"
69
+ s.description = "Ruby library for consuming Google, Yahoo!, Flickr and Windows Live contact APIs"
70
+ s.has_rdoc = true
71
+ s.authors = ["Mislav Marohnić"]
72
+ s.files = (Dir["lib/*"] + Dir["lib/**/*"] + Dir["vendor/*"] + Dir["*"]).reject_excluded_filenames
73
+ s.test_files = (Dir["spec/*"] + Dir["spec/**/*"]).reject_excluded_filenames
74
+
75
+ File.open("contacts.gemspec", "w") do |f|
76
+ f.write s.to_ruby
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+
83
+ class Array
84
+ def reject_excluded_filenames(pattern_list = [/.*~$/, /^\#.*\#$/, /.*\.gem$/])
85
+ reject {|a_file| pattern_list.any? {|pat| pat === a_file}}
86
+ end
87
+ end
data/contacts.gemspec ADDED
@@ -0,0 +1,27 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = %q{contacts}
3
+ s.version = "0.1.0"
4
+
5
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
6
+ s.authors = ["Mislav Marohni\304\207"]
7
+ s.date = %q{2008-12-08}
8
+ s.description = %q{Ruby library for consuming Google, Yahoo!, Flickr and Windows Live contact APIs}
9
+ s.email = %q{mislav.marohnic@gmail.com}
10
+ s.files = ["lib/config", "lib/contacts.rb", "lib/contacts", "lib/config/contacts.yml", "lib/contacts/flickr.rb", "lib/contacts/google.rb", "lib/contacts/version.rb", "lib/contacts/windows_live.rb", "lib/contacts/yahoo.rb", "vendor/windowslivelogin.rb", "MIT-LICENSE", "README.rdoc", "Rakefile", "lib", "spec", "vendor", "contacts.gemspec", "spec/contact_spec.rb", "spec/fake_net_http.rb", "spec/fake_web.rb", "spec/feeds", "spec/flickr", "spec/gmail", "spec/rcov.opts", "spec/spec.opts", "spec/spec_helper.rb", "spec/windows_live", "spec/yahoo", "spec/feeds/contacts.yml", "spec/feeds/flickr", "spec/feeds/flickr/auth.getFrob.xml", "spec/feeds/flickr/auth.getToken.xml", "spec/feeds/google-many.xml", "spec/feeds/google-single.xml", "spec/feeds/wl_contacts.xml", "spec/feeds/yh_contacts.txt", "spec/feeds/yh_credential.xml", "spec/flickr/auth_spec.rb", "spec/gmail/auth_spec.rb", "spec/gmail/fetching_spec.rb", "spec/windows_live/windows_live_spec.rb", "spec/yahoo/yahoo_spec.rb"]
11
+ s.has_rdoc = true
12
+ s.homepage = %q{http://github.com/mislav/contacts}
13
+ s.require_paths = ["lib"]
14
+ s.rubygems_version = %q{1.2.0}
15
+ s.summary = %q{Online contact APIs library}
16
+ s.test_files = ["spec/contact_spec.rb", "spec/fake_net_http.rb", "spec/fake_web.rb", "spec/feeds", "spec/flickr", "spec/gmail", "spec/rcov.opts", "spec/spec.opts", "spec/spec_helper.rb", "spec/windows_live", "spec/yahoo", "spec/contact_spec.rb", "spec/fake_net_http.rb", "spec/fake_web.rb", "spec/feeds", "spec/feeds/contacts.yml", "spec/feeds/flickr", "spec/feeds/flickr/auth.getFrob.xml", "spec/feeds/flickr/auth.getToken.xml", "spec/feeds/google-many.xml", "spec/feeds/google-single.xml", "spec/feeds/wl_contacts.xml", "spec/feeds/yh_contacts.txt", "spec/feeds/yh_credential.xml", "spec/flickr", "spec/flickr/auth_spec.rb", "spec/gmail", "spec/gmail/auth_spec.rb", "spec/gmail/fetching_spec.rb", "spec/rcov.opts", "spec/spec.opts", "spec/spec_helper.rb", "spec/windows_live", "spec/windows_live/windows_live_spec.rb", "spec/yahoo", "spec/yahoo/yahoo_spec.rb"]
17
+
18
+ if s.respond_to? :specification_version then
19
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
20
+ s.specification_version = 2
21
+
22
+ if current_version >= 3 then
23
+ else
24
+ end
25
+ else
26
+ end
27
+ end
@@ -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
data/lib/contacts.rb ADDED
@@ -0,0 +1,46 @@
1
+ require 'contacts/version'
2
+
3
+ module Contacts
4
+
5
+ Identifier = 'Ruby Contacts v' + VERSION::STRING
6
+
7
+ # An object that represents a single contact
8
+ class Contact
9
+ attr_reader :name, :username, :emails
10
+
11
+ def initialize(email, name = nil, username = nil)
12
+ @emails = []
13
+ @emails << email if email
14
+ @name = name
15
+ @username = username
16
+ end
17
+
18
+ def email
19
+ @emails.first
20
+ end
21
+
22
+ def inspect
23
+ %!#<Contacts::Contact "#{name}" (#{email})>!
24
+ end
25
+ end
26
+
27
+ def self.verbose?
28
+ 'irb' == $0
29
+ end
30
+
31
+ class Error < StandardError
32
+ end
33
+
34
+ class TooManyRedirects < Error
35
+ attr_reader :response, :location
36
+
37
+ MAX_REDIRECTS = 2
38
+
39
+ def initialize(response)
40
+ @response = response
41
+ @location = @response['Location']
42
+ super "exceeded maximum of #{MAX_REDIRECTS} redirects (Location: #{location})"
43
+ end
44
+ end
45
+
46
+ end
@@ -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,308 @@
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
+ protected
168
+
169
+ def in_chunks(options, what, chunk_size)
170
+ returns = []
171
+ offset = 0
172
+
173
+ begin
174
+ chunk = send(what, options.merge(:offset => offset, :limit => chunk_size))
175
+ returns.push(*chunk)
176
+ offset += chunk_size
177
+ end while chunk.size == chunk_size
178
+
179
+ returns
180
+ end
181
+
182
+ def response_body(response)
183
+ unless response['Content-Encoding'] == 'gzip'
184
+ response.body
185
+ else
186
+ gzipped = StringIO.new(response.body)
187
+ Zlib::GzipReader.new(gzipped).read
188
+ end
189
+ end
190
+
191
+ def parse_contacts(body)
192
+ doc = Hpricot::XML body
193
+ contacts_found = []
194
+
195
+ if updated_node = doc.at('/feed/updated')
196
+ @updated_string = updated_node.inner_text
197
+ end
198
+
199
+ (doc / '/feed/entry').each do |entry|
200
+ email_nodes = entry / 'gd:email[@address]'
201
+
202
+ unless email_nodes.empty?
203
+ title_node = entry.at('/title')
204
+ name = title_node ? title_node.inner_text : nil
205
+ contact = Contact.new(nil, name)
206
+ contact.emails.concat email_nodes.map { |e| e['address'].to_s }
207
+ contacts_found << contact
208
+ end
209
+ end
210
+
211
+ contacts_found
212
+ end
213
+
214
+ # Constructs a query string from a Hash object
215
+ def self.query_string(params)
216
+ params.inject([]) do |all, pair|
217
+ key, value = pair
218
+ unless value.nil?
219
+ value = case value
220
+ when TrueClass; '1'
221
+ when FalseClass; '0'
222
+ else value
223
+ end
224
+
225
+ all << "#{CGI.escape(key.to_s)}=#{CGI.escape(value.to_s)}"
226
+ end
227
+ all
228
+ end.join('&')
229
+ end
230
+
231
+ def translate_parameters(params)
232
+ params.inject({}) do |all, pair|
233
+ key, value = pair
234
+ unless value.nil?
235
+ key = case key
236
+ when :limit
237
+ 'max-results'
238
+ when :offset
239
+ value = value.to_i + 1
240
+ 'start-index'
241
+ when :order
242
+ all['sortorder'] = 'descending' if params[:descending].nil?
243
+ 'orderby'
244
+ when :descending
245
+ value = value ? 'descending' : 'ascending'
246
+ 'sortorder'
247
+ when :updated_after
248
+ value = value.strftime("%Y-%m-%dT%H:%M:%S%Z") if value.respond_to? :strftime
249
+ 'updated-min'
250
+ else key
251
+ end
252
+
253
+ all[key] = value
254
+ end
255
+ all
256
+ end
257
+ end
258
+
259
+ def self.authorization_header(token, client = false)
260
+ type = client ? 'GoogleLogin auth' : 'AuthSub token'
261
+ { 'Authorization' => %(#{type}="#{token}") }
262
+ end
263
+
264
+ def self.http_start(ssl = true)
265
+ port = ssl ? Net::HTTP::https_default_port : Net::HTTP::http_default_port
266
+ http = Net::HTTP.new(DOMAIN, port)
267
+ redirects = 0
268
+ if ssl
269
+ http.use_ssl = true
270
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
271
+ end
272
+ http.start
273
+
274
+ begin
275
+ response = yield(http)
276
+
277
+ loop do
278
+ inspect_response(response) if Contacts::verbose?
279
+
280
+ case response
281
+ when Net::HTTPSuccess
282
+ break response
283
+ when Net::HTTPRedirection
284
+ if redirects == TooManyRedirects::MAX_REDIRECTS
285
+ raise TooManyRedirects.new(response)
286
+ end
287
+ location = URI.parse response['Location']
288
+ puts "Redirected to #{location}"
289
+ response = http.get(location.path)
290
+ redirects += 1
291
+ else
292
+ response.error!
293
+ end
294
+ end
295
+ ensure
296
+ http.finish
297
+ end
298
+ end
299
+
300
+ def self.inspect_response(response, out = $stderr)
301
+ out.puts response.inspect
302
+ for name, value in response
303
+ out.puts "#{name}: #{value}"
304
+ end
305
+ out.puts "----\n#{response_body response}\n----" unless response.body.empty?
306
+ end
307
+ end
308
+ end